github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/cmd/gosbom/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/nextlinux/gosbom/cmd/gosbom/cli/eventloop" 11 "github.com/nextlinux/gosbom/cmd/gosbom/cli/options" 12 "github.com/nextlinux/gosbom/cmd/gosbom/cli/packages" 13 "github.com/nextlinux/gosbom/gosbom" 14 "github.com/nextlinux/gosbom/gosbom/event" 15 "github.com/nextlinux/gosbom/gosbom/event/monitor" 16 "github.com/nextlinux/gosbom/gosbom/formats/gosbomjson" 17 "github.com/nextlinux/gosbom/gosbom/formats/table" 18 "github.com/nextlinux/gosbom/gosbom/sbom" 19 "github.com/nextlinux/gosbom/gosbom/source" 20 "github.com/nextlinux/gosbom/internal/bus" 21 "github.com/nextlinux/gosbom/internal/config" 22 "github.com/nextlinux/gosbom/internal/log" 23 "github.com/nextlinux/gosbom/internal/ui" 24 "github.com/wagoodman/go-partybus" 25 "github.com/wagoodman/go-progress" 26 "golang.org/x/exp/slices" 27 28 "github.com/anchore/stereoscope" 29 ) 30 31 func Run(_ context.Context, app *config.Application, args []string) error { 32 err := ValidateOutputOptions(app) 33 if err != nil { 34 return err 35 } 36 37 // could be an image or a directory, with or without a scheme 38 // TODO: validate that source is image 39 userInput := args[0] 40 si, err := source.ParseInputWithNameVersion(userInput, app.Platform, app.SourceName, app.SourceVersion, app.DefaultImagePullSource) 41 if err != nil { 42 return fmt.Errorf("could not generate source input for packages command: %w", err) 43 } 44 45 if si.Scheme != source.ImageScheme { 46 return fmt.Errorf("attestations are only supported for oci images at this time") 47 } 48 49 eventBus := partybus.NewBus() 50 stereoscope.SetBus(eventBus) 51 gosbom.SetBus(eventBus) 52 subscription := eventBus.Subscribe() 53 54 return eventloop.EventLoop( 55 execWorker(app, *si), 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, si source.Input, errs chan error) (*sbom.SBOM, error) { 64 src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions) 65 if cleanup != nil { 66 defer cleanup() 67 } 68 if err != nil { 69 return nil, fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err) 70 } 71 72 s, err := packages.GenerateSBOM(src, errs, app) 73 if err != nil { 74 return nil, err 75 } 76 77 if s == nil { 78 return nil, fmt.Errorf("no SBOM produced for %q", si.UserInput) 79 } 80 81 return s, nil 82 } 83 84 //nolint:funlen 85 func execWorker(app *config.Application, si source.Input) <-chan error { 86 errs := make(chan error) 87 go func() { 88 defer close(errs) 89 defer bus.Publish(partybus.Event{Type: event.Exit}) 90 91 s, err := buildSBOM(app, si, errs) 92 if err != nil { 93 errs <- fmt.Errorf("unable to build SBOM: %w", err) 94 return 95 } 96 97 // note: ValidateOutputOptions ensures that there is no more than one output type 98 o := app.Outputs[0] 99 100 f, err := os.CreateTemp("", o) 101 if err != nil { 102 errs <- fmt.Errorf("unable to create temp file: %w", err) 103 return 104 } 105 defer os.Remove(f.Name()) 106 107 writer, err := options.MakeSBOMWriter(app.Outputs, f.Name(), app.OutputTemplatePath) 108 if err != nil { 109 errs <- fmt.Errorf("unable to create SBOM writer: %w", err) 110 return 111 } 112 113 if err := writer.Write(*s); err != nil { 114 errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err) 115 return 116 } 117 118 // TODO: what other validation here besides binary name? 119 cmd := "cosign" 120 if !commandExists(cmd) { 121 errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed") 122 return 123 } 124 125 // Select Cosign predicate type based on defined output type 126 // As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go 127 var predicateType string 128 switch strings.ToLower(o) { 129 case "cyclonedx-json": 130 predicateType = "cyclonedx" 131 case "spdx-tag-value", "spdx-tv": 132 predicateType = "spdx" 133 case "spdx-json", "json": 134 predicateType = "spdxjson" 135 default: 136 predicateType = "custom" 137 } 138 139 args := []string{"attest", si.UserInput, "--predicate", f.Name(), "--type", predicateType} 140 if app.Attest.Key != "" { 141 args = append(args, "--key", app.Attest.Key) 142 } 143 144 execCmd := exec.Command(cmd, args...) 145 execCmd.Env = os.Environ() 146 if app.Attest.Key != "" { 147 execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", app.Attest.Password)) 148 } else { 149 // no key provided, use cosign's keyless mode 150 execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1") 151 } 152 153 log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation") 154 155 // bus adapter for ui to hook into stdout via an os pipe 156 r, w, err := os.Pipe() 157 if err != nil { 158 errs <- fmt.Errorf("unable to create os pipe: %w", err) 159 return 160 } 161 defer w.Close() 162 163 mon := progress.NewManual(-1) 164 165 bus.Publish( 166 partybus.Event{ 167 Type: event.AttestationStarted, 168 Source: monitor.GenericTask{ 169 Title: monitor.Title{ 170 Default: "Create attestation", 171 WhileRunning: "Creating attestation", 172 OnSuccess: "Created attestation", 173 }, 174 Context: "cosign", 175 }, 176 Value: &monitor.ShellProgress{ 177 Reader: r, 178 Manual: mon, 179 }, 180 }, 181 ) 182 183 execCmd.Stdout = w 184 execCmd.Stderr = w 185 186 // attest the SBOM 187 err = execCmd.Run() 188 if err != nil { 189 mon.SetError(err) 190 errs <- fmt.Errorf("unable to attest SBOM: %w", err) 191 return 192 } 193 194 mon.SetCompleted() 195 }() 196 return errs 197 } 198 199 func ValidateOutputOptions(app *config.Application) error { 200 err := packages.ValidateOutputOptions(app) 201 if err != nil { 202 return err 203 } 204 205 if len(app.Outputs) > 1 { 206 return fmt.Errorf("multiple SBOM format is not supported for attest at this time") 207 } 208 209 // cannot use table as default output format when using template output 210 if slices.Contains(app.Outputs, table.ID.String()) { 211 app.Outputs = []string{gosbomjson.ID.String()} 212 } 213 214 return nil 215 } 216 217 func commandExists(cmd string) bool { 218 _, err := exec.LookPath(cmd) 219 return err == nil 220 }