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