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 }