github.com/triarius/goreleaser@v1.12.5/internal/pipe/sbom/sbom.go (about)

     1  package sbom
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/caarlos0/log"
    13  	"github.com/triarius/goreleaser/internal/artifact"
    14  	"github.com/triarius/goreleaser/internal/gio"
    15  	"github.com/triarius/goreleaser/internal/ids"
    16  	"github.com/triarius/goreleaser/internal/logext"
    17  	"github.com/triarius/goreleaser/internal/semerrgroup"
    18  	"github.com/triarius/goreleaser/internal/tmpl"
    19  	"github.com/triarius/goreleaser/pkg/config"
    20  	"github.com/triarius/goreleaser/pkg/context"
    21  )
    22  
    23  // Environment variables to pass through to exec
    24  var passthroughEnvVars = []string{"HOME", "USER", "USERPROFILE", "TMPDIR", "TMP", "TEMP", "PATH"}
    25  
    26  // Pipe that catalogs common artifacts as an SBOM.
    27  type Pipe struct{}
    28  
    29  func (Pipe) String() string { return "cataloging artifacts" }
    30  func (Pipe) Skip(ctx *context.Context) bool {
    31  	return ctx.SkipSBOMCataloging || len(ctx.Config.SBOMs) == 0
    32  }
    33  
    34  // Default sets the Pipes defaults.
    35  func (Pipe) Default(ctx *context.Context) error {
    36  	ids := ids.New("sboms")
    37  	for i := range ctx.Config.SBOMs {
    38  		cfg := &ctx.Config.SBOMs[i]
    39  		if err := setConfigDefaults(cfg); err != nil {
    40  			return err
    41  		}
    42  		ids.Inc(cfg.ID)
    43  	}
    44  	return ids.Validate()
    45  }
    46  
    47  func setConfigDefaults(cfg *config.SBOM) error {
    48  	if cfg.Cmd == "" {
    49  		cfg.Cmd = "syft"
    50  	}
    51  	if cfg.Artifacts == "" {
    52  		cfg.Artifacts = "archive"
    53  	}
    54  	if len(cfg.Documents) == 0 {
    55  		switch cfg.Artifacts {
    56  		case "binary":
    57  			cfg.Documents = []string{"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"}
    58  		case "any":
    59  			cfg.Documents = []string{}
    60  		default:
    61  			cfg.Documents = []string{"{{ .ArtifactName }}.sbom"}
    62  		}
    63  	}
    64  	if cfg.Cmd == "syft" {
    65  		if len(cfg.Args) == 0 {
    66  			cfg.Args = []string{"$artifact", "--file", "$document", "--output", "spdx-json"}
    67  		}
    68  		if len(cfg.Env) == 0 && (cfg.Artifacts == "source" || cfg.Artifacts == "archive") {
    69  			cfg.Env = []string{
    70  				"SYFT_FILE_METADATA_CATALOGER_ENABLED=true",
    71  			}
    72  		}
    73  	}
    74  	if cfg.ID == "" {
    75  		cfg.ID = "default"
    76  	}
    77  
    78  	if cfg.Artifacts != "any" && len(cfg.Documents) > 1 {
    79  		return fmt.Errorf("multiple SBOM outputs when artifacts=%q is unsupported", cfg.Artifacts)
    80  	}
    81  	return nil
    82  }
    83  
    84  // Run executes the Pipe.
    85  func (Pipe) Run(ctx *context.Context) error {
    86  	g := semerrgroup.New(ctx.Parallelism)
    87  	for _, cfg := range ctx.Config.SBOMs {
    88  		g.Go(catalogTask(ctx, cfg))
    89  	}
    90  	return g.Wait()
    91  }
    92  
    93  func catalogTask(ctx *context.Context, cfg config.SBOM) func() error {
    94  	return func() error {
    95  		var filters []artifact.Filter
    96  		switch cfg.Artifacts {
    97  		case "source":
    98  			filters = append(filters, artifact.ByType(artifact.UploadableSourceArchive))
    99  			if len(cfg.IDs) > 0 {
   100  				log.Warn("when artifacts is `source`, `ids` has no effect. ignoring")
   101  			}
   102  		case "archive":
   103  			filters = append(filters, artifact.ByType(artifact.UploadableArchive))
   104  		case "binary":
   105  			filters = append(filters, artifact.ByBinaryLikeArtifacts(ctx.Artifacts))
   106  		case "package":
   107  			filters = append(filters, artifact.ByType(artifact.LinuxPackage))
   108  		case "any":
   109  			newArtifacts, err := catalogArtifact(ctx, cfg, nil)
   110  			if err != nil {
   111  				return err
   112  			}
   113  			for _, newArtifact := range newArtifacts {
   114  				ctx.Artifacts.Add(newArtifact)
   115  			}
   116  			return nil
   117  		default:
   118  			return fmt.Errorf("invalid list of artifacts to catalog: %s", cfg.Artifacts)
   119  		}
   120  
   121  		if len(cfg.IDs) > 0 {
   122  			filters = append(filters, artifact.ByIDs(cfg.IDs...))
   123  		}
   124  		artifacts := ctx.Artifacts.Filter(artifact.And(filters...)).List()
   125  		return catalog(ctx, cfg, artifacts)
   126  	}
   127  }
   128  
   129  func catalog(ctx *context.Context, cfg config.SBOM, artifacts []*artifact.Artifact) error {
   130  	for _, a := range artifacts {
   131  		newArtifacts, err := catalogArtifact(ctx, cfg, a)
   132  		if err != nil {
   133  			return err
   134  		}
   135  		for _, newArtifact := range newArtifacts {
   136  			ctx.Artifacts.Add(newArtifact)
   137  		}
   138  	}
   139  	return nil
   140  }
   141  
   142  func subprocessDistPath(distDir string, pathRelativeToCwd string) (string, error) {
   143  	cwd, err := os.Getwd()
   144  	if err != nil {
   145  		return "", err
   146  	}
   147  	if !filepath.IsAbs(distDir) {
   148  		distDir, err = filepath.Abs(distDir)
   149  		if err != nil {
   150  			return "", err
   151  		}
   152  	}
   153  	relativePath, err := filepath.Rel(cwd, distDir)
   154  	if err != nil {
   155  		return "", err
   156  	}
   157  	return strings.TrimPrefix(pathRelativeToCwd, relativePath+string(filepath.Separator)), nil
   158  }
   159  
   160  func catalogArtifact(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact) ([]*artifact.Artifact, error) {
   161  	artifactDisplayName := "(any)"
   162  	args, envs, paths, err := applyTemplate(ctx, cfg, a)
   163  	if err != nil {
   164  		return nil, fmt.Errorf("cataloging artifacts failed: %w", err)
   165  	}
   166  
   167  	if a != nil {
   168  		artifactDisplayName = a.Path
   169  	}
   170  
   171  	var names []string
   172  	for _, p := range paths {
   173  		names = append(names, filepath.Base(p))
   174  	}
   175  
   176  	fields := log.Fields{"cmd": cfg.Cmd, "artifact": artifactDisplayName, "sboms": strings.Join(names, ", ")}
   177  
   178  	// The GoASTScanner flags this as a security risk.
   179  	// However, this works as intended. The nosec annotation
   180  	// tells the scanner to ignore this.
   181  	// #nosec
   182  	cmd := exec.CommandContext(ctx, cfg.Cmd, args...)
   183  	cmd.Env = []string{}
   184  	for _, key := range passthroughEnvVars {
   185  		if value := os.Getenv(key); value != "" {
   186  			cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
   187  		}
   188  	}
   189  	cmd.Env = append(cmd.Env, envs...)
   190  	cmd.Dir = ctx.Config.Dist
   191  
   192  	var b bytes.Buffer
   193  	w := gio.Safe(&b)
   194  	cmd.Stderr = io.MultiWriter(logext.NewWriter(fields, logext.Error), w)
   195  	cmd.Stdout = io.MultiWriter(logext.NewWriter(fields, logext.Info), w)
   196  
   197  	log.WithFields(fields).Info("cataloging")
   198  	if err := cmd.Run(); err != nil {
   199  		return nil, fmt.Errorf("cataloging artifacts: %s failed: %w: %s", cfg.Cmd, err, b.String())
   200  	}
   201  
   202  	var artifacts []*artifact.Artifact
   203  
   204  	for _, path := range paths {
   205  		if !filepath.IsAbs(path) {
   206  			path = filepath.Join(ctx.Config.Dist, path)
   207  		}
   208  
   209  		matches, err := filepath.Glob(path)
   210  		if err != nil {
   211  			return nil, fmt.Errorf("cataloging artifacts: failed to find SBOM artifact %q: %w", path, err)
   212  		}
   213  		for _, match := range matches {
   214  			artifacts = append(artifacts, &artifact.Artifact{
   215  				Type: artifact.SBOM,
   216  				Name: filepath.Base(path),
   217  				Path: match,
   218  				Extra: map[string]interface{}{
   219  					artifact.ExtraID: cfg.ID,
   220  				},
   221  			})
   222  		}
   223  
   224  	}
   225  
   226  	return artifacts, nil
   227  }
   228  
   229  func applyTemplate(ctx *context.Context, cfg config.SBOM, a *artifact.Artifact) ([]string, []string, []string, error) {
   230  	env := ctx.Env.Copy()
   231  	var extraEnvs []string
   232  	templater := tmpl.New(ctx).WithEnv(env)
   233  
   234  	if a != nil {
   235  		procPath, err := subprocessDistPath(ctx.Config.Dist, a.Path)
   236  		if err != nil {
   237  			return nil, nil, nil, fmt.Errorf("cataloging artifacts failed: cannot determine artifact path for %q: %w", a.Path, err)
   238  		}
   239  		extraEnvs = appendExtraEnv("artifact", procPath, extraEnvs, env)
   240  		extraEnvs = appendExtraEnv("artifactID", a.ID(), extraEnvs, env)
   241  
   242  		templater = templater.WithArtifact(a, nil)
   243  	}
   244  
   245  	for _, keyValue := range cfg.Env {
   246  		renderedKeyValue, err := templater.Apply(expand(keyValue, env))
   247  		if err != nil {
   248  			return nil, nil, nil, fmt.Errorf("env %q: invalid template: %w", keyValue, err)
   249  		}
   250  		extraEnvs = append(extraEnvs, renderedKeyValue)
   251  
   252  		k, v, _ := strings.Cut(renderedKeyValue, "=")
   253  		env[k] = v
   254  	}
   255  
   256  	var paths []string
   257  	for idx, sbom := range cfg.Documents {
   258  		input := expand(sbom, env)
   259  		if !filepath.IsAbs(input) {
   260  			// assume any absolute path is handled correctly and assume that any relative path is not already
   261  			// adjusted to reference the dist path
   262  			input = filepath.Join(ctx.Config.Dist, input)
   263  		}
   264  
   265  		path, err := templater.Apply(input)
   266  		if err != nil {
   267  			return nil, nil, nil, fmt.Errorf("input %q: invalid template: %w", input, err)
   268  		}
   269  
   270  		path, err = filepath.Abs(path)
   271  		if err != nil {
   272  			return nil, nil, nil, fmt.Errorf("unable to create artifact path %q: %w", sbom, err)
   273  		}
   274  
   275  		procPath, err := subprocessDistPath(ctx.Config.Dist, path)
   276  		if err != nil {
   277  			return nil, nil, nil, fmt.Errorf("cannot determine document path for %q: %w", path, err)
   278  		}
   279  
   280  		extraEnvs = appendExtraEnv(fmt.Sprintf("document%d", idx), procPath, extraEnvs, env)
   281  		if idx == 0 {
   282  			extraEnvs = appendExtraEnv("document", procPath, extraEnvs, env)
   283  		}
   284  
   285  		paths = append(paths, procPath)
   286  	}
   287  
   288  	// nolint:prealloc
   289  	var args []string
   290  	for _, arg := range cfg.Args {
   291  		renderedArg, err := templater.Apply(expand(arg, env))
   292  		if err != nil {
   293  			return nil, nil, nil, fmt.Errorf("arg %q: invalid template: %w", arg, err)
   294  		}
   295  		args = append(args, renderedArg)
   296  	}
   297  
   298  	return args, extraEnvs, paths, nil
   299  }
   300  
   301  func appendExtraEnv(key, value string, envs []string, env map[string]string) []string {
   302  	env[key] = value
   303  	return append(envs, fmt.Sprintf("%s=%s", key, value))
   304  }
   305  
   306  func expand(s string, env map[string]string) string {
   307  	return os.Expand(s, func(key string) string {
   308  		return env[key]
   309  	})
   310  }