github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/cmd/syft/cli/attest/attest.go (about) 1 package attest 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "os/exec" 8 "strings" 9 10 "github.com/wagoodman/go-partybus" 11 "github.com/wagoodman/go-progress" 12 "golang.org/x/exp/slices" 13 14 "github.com/anchore/stereoscope" 15 "github.com/anchore/stereoscope/pkg/image" 16 "github.com/kastenhq/syft/cmd/syft/cli/eventloop" 17 "github.com/kastenhq/syft/cmd/syft/cli/options" 18 "github.com/kastenhq/syft/cmd/syft/cli/packages" 19 "github.com/kastenhq/syft/cmd/syft/internal/ui" 20 "github.com/kastenhq/syft/internal/bus" 21 "github.com/kastenhq/syft/internal/config" 22 "github.com/kastenhq/syft/internal/file" 23 "github.com/kastenhq/syft/internal/log" 24 "github.com/kastenhq/syft/syft" 25 "github.com/kastenhq/syft/syft/event" 26 "github.com/kastenhq/syft/syft/event/monitor" 27 "github.com/kastenhq/syft/syft/formats/syftjson" 28 "github.com/kastenhq/syft/syft/formats/table" 29 "github.com/kastenhq/syft/syft/sbom" 30 "github.com/kastenhq/syft/syft/source" 31 ) 32 33 func Run(_ context.Context, app *config.Application, args []string) error { 34 err := ValidateOutputOptions(app) 35 if err != nil { 36 return err 37 } 38 39 // note: must be a container image 40 userInput := args[0] 41 42 _, err = exec.LookPath("cosign") 43 if err != nil { 44 // when cosign is not installed the error will be rendered like so: 45 // 2023/06/30 08:31:52 error during command execution: 'syft attest' requires cosign to be installed: exec: "cosign": executable file not found in $PATH 46 return fmt.Errorf("'syft attest' requires cosign to be installed: %w", err) 47 } 48 49 eventBus := partybus.NewBus() 50 stereoscope.SetBus(eventBus) 51 syft.SetBus(eventBus) 52 subscription := eventBus.Subscribe() 53 54 return eventloop.EventLoop( 55 execWorker(app, userInput), 56 eventloop.SetupSignals(), 57 subscription, 58 stereoscope.Cleanup, 59 ui.Select(options.IsVerbose(app), app.Quiet)..., 60 ) 61 } 62 63 func buildSBOM(app *config.Application, userInput string, errs chan error) (*sbom.SBOM, error) { 64 cfg := source.DetectConfig{ 65 DefaultImageSource: app.DefaultImagePullSource, 66 } 67 detection, err := source.Detect(userInput, cfg) 68 if err != nil { 69 return nil, fmt.Errorf("could not deteremine source: %w", err) 70 } 71 72 if detection.IsContainerImage() { 73 return nil, fmt.Errorf("attestations are only supported for oci images at this time") 74 } 75 76 var platform *image.Platform 77 78 if app.Platform != "" { 79 platform, err = image.NewPlatform(app.Platform) 80 if err != nil { 81 return nil, fmt.Errorf("invalid platform: %w", err) 82 } 83 } 84 85 hashers, err := file.Hashers(app.Source.File.Digests...) 86 if err != nil { 87 return nil, fmt.Errorf("invalid hash: %w", err) 88 } 89 90 src, err := detection.NewSource( 91 source.DetectionSourceConfig{ 92 Alias: source.Alias{ 93 Name: app.Source.Name, 94 Version: app.Source.Version, 95 }, 96 RegistryOptions: app.Registry.ToOptions(), 97 Platform: platform, 98 Exclude: source.ExcludeConfig{ 99 Paths: app.Exclusions, 100 }, 101 DigestAlgorithms: hashers, 102 BasePath: app.BasePath, 103 }, 104 ) 105 106 if src != nil { 107 defer src.Close() 108 } 109 if err != nil { 110 return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) 111 } 112 113 s, err := packages.GenerateSBOM(src, errs, app) 114 if err != nil { 115 return nil, err 116 } 117 118 if s == nil { 119 return nil, fmt.Errorf("no SBOM produced for %q", userInput) 120 } 121 122 return s, nil 123 } 124 125 //nolint:funlen 126 func execWorker(app *config.Application, userInput string) <-chan error { 127 errs := make(chan error) 128 go func() { 129 defer close(errs) 130 defer bus.Exit() 131 132 s, err := buildSBOM(app, userInput, errs) 133 if err != nil { 134 errs <- fmt.Errorf("unable to build SBOM: %w", err) 135 return 136 } 137 138 // note: ValidateOutputOptions ensures that there is no more than one output type 139 o := app.Outputs[0] 140 141 f, err := os.CreateTemp("", o) 142 if err != nil { 143 errs <- fmt.Errorf("unable to create temp file: %w", err) 144 return 145 } 146 defer os.Remove(f.Name()) 147 148 writer, err := options.MakeSBOMWriter(app.Outputs, f.Name(), app.OutputTemplatePath) 149 if err != nil { 150 errs <- fmt.Errorf("unable to create SBOM writer: %w", err) 151 return 152 } 153 154 if err := writer.Write(*s); err != nil { 155 errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err) 156 return 157 } 158 159 // TODO: what other validation here besides binary name? 160 cmd := "cosign" 161 if !commandExists(cmd) { 162 errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed") 163 return 164 } 165 166 // Select Cosign predicate type based on defined output type 167 // As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go 168 var predicateType string 169 switch strings.ToLower(o) { 170 case "cyclonedx-json": 171 predicateType = "cyclonedx" 172 case "spdx-tag-value", "spdx-tv": 173 predicateType = "spdx" 174 case "spdx-json", "json": 175 predicateType = "spdxjson" 176 default: 177 predicateType = "custom" 178 } 179 180 args := []string{"attest", userInput, "--predicate", f.Name(), "--type", predicateType} 181 if app.Attest.Key != "" { 182 args = append(args, "--key", app.Attest.Key) 183 } 184 185 execCmd := exec.Command(cmd, args...) 186 execCmd.Env = os.Environ() 187 if app.Attest.Key != "" { 188 execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", app.Attest.Password)) 189 } else { 190 // no key provided, use cosign's keyless mode 191 execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1") 192 } 193 194 log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation") 195 196 // bus adapter for ui to hook into stdout via an os pipe 197 r, w, err := os.Pipe() 198 if err != nil { 199 errs <- fmt.Errorf("unable to create os pipe: %w", err) 200 return 201 } 202 defer w.Close() 203 204 mon := progress.NewManual(-1) 205 206 bus.Publish( 207 partybus.Event{ 208 Type: event.AttestationStarted, 209 Source: monitor.GenericTask{ 210 Title: monitor.Title{ 211 Default: "Create attestation", 212 WhileRunning: "Creating attestation", 213 OnSuccess: "Created attestation", 214 }, 215 Context: "cosign", 216 }, 217 Value: &monitor.ShellProgress{ 218 Reader: r, 219 Progressable: mon, 220 }, 221 }, 222 ) 223 224 execCmd.Stdout = w 225 execCmd.Stderr = w 226 227 // attest the SBOM 228 err = execCmd.Run() 229 if err != nil { 230 mon.SetError(err) 231 errs <- fmt.Errorf("unable to attest SBOM: %w", err) 232 return 233 } 234 235 mon.SetCompleted() 236 }() 237 return errs 238 } 239 240 func ValidateOutputOptions(app *config.Application) error { 241 err := packages.ValidateOutputOptions(app) 242 if err != nil { 243 return err 244 } 245 246 if len(app.Outputs) > 1 { 247 return fmt.Errorf("multiple SBOM format is not supported for attest at this time") 248 } 249 250 // cannot use table as default output format when using template output 251 if slices.Contains(app.Outputs, table.ID.String()) { 252 app.Outputs = []string{syftjson.ID.String()} 253 } 254 255 return nil 256 } 257 258 func commandExists(cmd string) bool { 259 _, err := exec.LookPath(cmd) 260 return err == nil 261 }