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 }