github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/cmd/sign/sign.go (about) 1 /* 2 * Copyright (c) 2018-2020 vChain, Inc. All Rights Reserved. 3 * This software is released under GPL3. 4 * The full license information can be found under: 5 * https://www.gnu.org/licenses/gpl-3.0.en.html 6 * 7 */ 8 9 package sign 10 11 import ( 12 "bufio" 13 "errors" 14 "fmt" 15 "os" 16 "path/filepath" 17 "strings" 18 19 "github.com/vchain-us/vcn/pkg/signature" 20 21 "github.com/caarlos0/spin" 22 "github.com/fatih/color" 23 "github.com/schollz/progressbar/v3" 24 "github.com/spf13/cobra" 25 "github.com/spf13/viper" 26 "github.com/vchain-us/vcn/pkg/extractor/wildcard" 27 "golang.org/x/crypto/ssh/terminal" 28 29 "github.com/vchain-us/ledger-compliance-go/schema" 30 "github.com/vchain-us/vcn/internal/assert" 31 vcnerr "github.com/vchain-us/vcn/internal/errors" 32 "github.com/vchain-us/vcn/pkg/api" 33 "github.com/vchain-us/vcn/pkg/bom" 34 "github.com/vchain-us/vcn/pkg/bom/artifact" 35 "github.com/vchain-us/vcn/pkg/bom/docker" 36 "github.com/vchain-us/vcn/pkg/cicontext" 37 "github.com/vchain-us/vcn/pkg/cmd/internal/cli" 38 "github.com/vchain-us/vcn/pkg/cmd/internal/types" 39 "github.com/vchain-us/vcn/pkg/cmd/verify" 40 "github.com/vchain-us/vcn/pkg/extractor" 41 "github.com/vchain-us/vcn/pkg/extractor/dir" 42 "github.com/vchain-us/vcn/pkg/meta" 43 "github.com/vchain-us/vcn/pkg/store" 44 "github.com/vchain-us/vcn/pkg/uri" 45 ) 46 47 const longDescFooter = ` 48 49 VCN_NOTARIZATION_PASSWORD env var can be used to pass the 50 required notarization password in a non-interactive environment. 51 ` 52 53 const helpMsgFooter = ` 54 ARG must be one of: 55 wildcard 56 file 57 directory 58 file://<file> 59 dir://<directory> 60 git://<repository> 61 docker://<image> 62 podman://<image> 63 wildcard://"*" 64 javacom://<java project component> 65 nodecom://<node component> 66 gocom://<Go module in name@version format> 67 pythoncom://<Python module in name@version format> 68 dotnetcom://<.Net module in name@version format> 69 ` 70 71 // NewCommand returns the cobra command for `vcn sign` 72 func NewCommand() *cobra.Command { 73 cmd := makeCommand() 74 cmd.Flags().Bool("create-alert", false, "if set, an alert will be created (config will be stored into the .vcn dir)") 75 cmd.Flags().String("alert-name", "", "set the alert name (ignored if --create-alert is not set)") 76 cmd.Flags().String("alert-email", "", "set the alert email recipient (ignored if --create-alert is not set)") 77 cmd.Flags().Bool("bom", false, "auto-notarize asset dependencies and link dependencies to the asset") 78 cmd.Flags().String("bom-file", "", "use specified BoM file rather than resolve dependencies") 79 cmd.Flags().String("bom-signerID", "", "signerID to use for authenticating dependencies") 80 cmd.Flags().Bool("bom-deps-only", false, "notarize only the dependencies, not the asset") 81 cmd.Flags().StringSlice("bom-container-binary", []string{}, "list of binaries to be executed inside the container - only the relevant dependencies will be processed") 82 cmd.Flags().StringSlice("bom-hashes", []string{}, "hashes of the dependencies (disables automatic dependency resolution)") 83 cmd.Flags().Uint("bom-batch-size", 10, "By default BoM dependencies are authenticated/notarized in batches of up to 10 dependencies each. Use this flag to set a different batch size. A value of 0 will disable batching (all dependencies will be authenticated/notarized at once).") 84 // BoM output options 85 cmd.Flags().Bool("bom-debug", false, "show extra debug info for BoM processing, also disable progress indicators") 86 cmd.Flags().String("bom-spdx", "", "name of the file to output BoM in SPDX format") 87 cmd.Flags().String("bom-cyclonedx-json", "", "name of the file to output BoM in CycloneDX JSON format") 88 cmd.Flags().String("bom-cyclonedx-xml", "", "name of the file to output BoM in CycloneDX XML format") 89 return cmd 90 } 91 92 func makeCommand() *cobra.Command { 93 cmd := &cobra.Command{ 94 Use: "notarize", 95 Aliases: []string{"n", "sign", "s"}, 96 Short: "Notarize an asset onto the blockchain", 97 Long: ` 98 Notarize an asset onto the blockchain. 99 100 Notarization calculates the SHA-256 hash of a digital asset 101 (file, directory, container's image). 102 The hash (not the asset) and the desired status of TRUSTED are then 103 cryptographically signed by the signer's secret (private key). 104 Next, these signed objects are sent to the blockchain where the signer’s 105 trust level and a timestamp are added. 106 When complete, a new blockchain entry is created that binds the asset’s 107 signed hash, signed status, level, and timestamp together. 108 109 Note that your assets will not be uploaded. They will be processed locally. 110 111 Assets are referenced by passed ARG with notarization only accepting 112 1 ARG at a time. 113 114 Pipe mode: 115 If '-' is provided (echo my-file | vcn n -) stdin is read and parsed. Only pipe ARGs are processed. 116 117 Environment variables: 118 VCN_USER= 119 VCN_PASSWORD= 120 VCN_NOTARIZATION_PASSWORD= 121 VCN_NOTARIZATION_PASSWORD_EMPTY= 122 VCN_OTP= 123 VCN_OTP_EMPTY= 124 VCN_LC_HOST= 125 VCN_LC_PORT= 126 VCN_LC_CERT= 127 VCN_LC_SKIP_TLS_VERIFY=false 128 VCN_LC_NO_TLS=false 129 VCN_LC_API_KEY= 130 VCN_SIGNING_PUB_KEY_FILE= 131 VCN_SIGNING_PUB_KEY= 132 VCN_ENFORCE_SIGNATURE_VERIFY= 133 ` + helpMsgFooter, 134 PreRunE: func(cmd *cobra.Command, args []string) error { 135 return viper.BindPFlags(cmd.Flags()) 136 }, 137 RunE: func(cmd *cobra.Command, args []string) error { 138 if pipeMode() && len(args) > 0 && args[0] == "-" { 139 args = make([]string, 0) 140 scanner := bufio.NewScanner(os.Stdin) 141 scanner.Split(bufio.ScanWords) 142 for scanner.Scan() { 143 token := scanner.Bytes() 144 args = append(args, string(token)) 145 } 146 if err := scanner.Err(); err != nil { 147 return fmt.Errorf("error parsing stdin input: %s", err) 148 } 149 } 150 return runSignWithState(cmd, args, meta.StatusTrusted) 151 }, 152 Args: noArgsWhenHashOrPipe, 153 Example: `vcn notarize my-file" 154 vcn notarize -r "*.md" 155 echo my-file | vcn n -`, 156 } 157 158 cmd.Flags().VarP(make(mapOpts), "attr", "a", "add user defined attributes (repeat --attr for multiple entries). Special attributes: allowdownload=user1,user2 forbids attachments downloads for the CNIL specified user(name)s") 159 cmd.Flags().Bool("ci-attr", false, meta.VcnLcCIAttribDesc) 160 cmd.Flags().StringP("name", "n", "", "set the asset name") 161 cmd.Flags().BoolP("public", "p", false, "when notarized as public, the asset name and metadata will be visible to everyone") 162 cmd.Flags().String("hash", "", "specify the hash instead of using an asset, if set no ARG(s) can be used") 163 cmd.Flags().Bool("no-ignore-file", false, "if set, .vcnignore will be not written inside the targeted dir (affects dir:// only)") 164 cmd.Flags().Bool("read-only", false, "if set, no files will be written into the targeted dir (affects dir:// only)") 165 cmd.Flags().BoolP("recursive", "r", false, "if set, wildcard usage will walk inside subdirectories of provided path") 166 cmd.Flags().String("lc-host", "", meta.VcnLcHostFlagDesc) 167 cmd.Flags().String("lc-port", "443", meta.VcnLcPortFlagDesc) 168 cmd.Flags().String("lc-cert", "", meta.VcnLcCertPathDesc) 169 cmd.Flags().Bool("lc-skip-tls-verify", false, meta.VcnLcSkipTlsVerifyDesc) 170 cmd.Flags().Bool("lc-no-tls", false, meta.VcnLcNoTlsDesc) 171 cmd.Flags().String("lc-api-key", "", meta.VcnLcApiKeyDesc) 172 cmd.Flags().StringArray("attach", nil, meta.VcnLcAttachDesc) 173 cmd.Flags().Bool("bom-cascade", false, "cascade the operation to all assets that include the asset being processed") 174 cmd.Flags().Bool("bom-force", false, "force notarization of untrusted dependencies, force cascade operation") 175 cmd.Flags().Bool("bom-container-hash", false, "when --bom-container-binary is specified, use container image hash, not binary hash") 176 cmd.Flags().String("github-token", "", "Github OAuth token for querying BoM Github package details. Either authenticated or not, requests are subject to Github limits") 177 178 cmd.SetUsageTemplate( 179 strings.Replace(cmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}} ARG", 1), 180 ) 181 cmd.Flags().String("signing-pub-key-file", "", meta.VcnSigningPubKeyFileNameDesc) 182 cmd.Flags().String("signing-pub-key", "", meta.VcnSigningPubKeyDesc) 183 cmd.Flags().Bool("enforce-signature-verify", false, meta.VcnEnforceSignatureVerifyDesc) 184 cmd.Flags().Bool("compress", false, "store all specified attachments as a single ZIP archive") 185 186 return cmd 187 } 188 189 func runSignWithState(cmd *cobra.Command, args []string, state meta.Status) error { 190 // default extractors options 191 extractorOptions := []extractor.Option{} 192 193 noIgnoreFile, err := cmd.Flags().GetBool("no-ignore-file") 194 if err != nil { 195 return err 196 } 197 readOnly, err := cmd.Flags().GetBool("read-only") 198 if err != nil { 199 return err 200 } 201 if readOnly { 202 noIgnoreFile = true 203 } 204 if !noIgnoreFile { 205 extractorOptions = append(extractorOptions, dir.WithIgnoreFileInit()) 206 extractorOptions = append(extractorOptions, dir.WithSkipIgnoreFileErr()) 207 } 208 209 recursive, err := cmd.Flags().GetBool("recursive") 210 if err != nil { 211 return err 212 } 213 if recursive { 214 extractorOptions = append(extractorOptions, wildcard.WithRecursive()) 215 } 216 var alert *alertOptions 217 if hasCreateAlert := cmd.Flags().Lookup("create-alert"); hasCreateAlert != nil { 218 createAlert, err := cmd.Flags().GetBool("create-alert") 219 if err != nil { 220 return err 221 } 222 if createAlert { 223 alert = &alertOptions{ 224 arg: args[0], 225 } 226 alert.name, _ = cmd.Flags().GetString("alert-name") 227 if err != nil { 228 return err 229 } 230 alert.email, _ = cmd.Flags().GetString("alert-email") 231 if err != nil { 232 return err 233 } 234 } 235 } 236 237 var hash string 238 if hashFlag := cmd.Flags().Lookup("hash"); hashFlag != nil { 239 var err error 240 hash, err = cmd.Flags().GetString("hash") 241 if err != nil { 242 return err 243 } 244 } 245 246 public, err := cmd.Flags().GetBool("public") 247 if err != nil { 248 return err 249 } 250 251 output, err := cmd.Flags().GetString("output") 252 if err != nil { 253 return err 254 } 255 256 silentMode, err := cmd.Flags().GetBool("silent") 257 if err != nil { 258 return err 259 } 260 261 name, err := cmd.Flags().GetString("name") 262 if err != nil { 263 return err 264 } 265 266 metadata := cmd.Flags().Lookup("attr").Value.(mapOpts).StringToInterface() 267 268 // @todo use dependency injection 269 cs := cicontext.NewContextSaver() 270 271 if viper.GetBool("ci-attr") { 272 cicontext.ExtendMetadata(metadata, cs.GetCIContextMetadata()) 273 } 274 275 cmd.SilenceUsage = true 276 277 lcHost := viper.GetString("lc-host") 278 lcPort := viper.GetString("lc-port") 279 lcCert := viper.GetString("lc-cert") 280 skipTlsVerify := viper.GetBool("lc-skip-tls-verify") 281 noTls := viper.GetBool("lc-no-tls") 282 lcApiKey := viper.GetString("lc-api-key") 283 284 lcVerbose := viper.GetBool("verbose") 285 286 signingPubKey, skipLocalPubKeyComp, err := signature.PrepareSignatureParams( 287 viper.GetString("signing-pub-key"), 288 viper.GetString("signing-pub-key-file")) 289 if err != nil { 290 return err 291 } 292 enforceSignatureVerify := viper.GetBool("enforce-signature-verify") 293 294 // todo add attachment validator. Deny ":" misuses in input string 295 attachments, err := cmd.Flags().GetStringArray("attach") 296 if err != nil { 297 return err 298 } 299 300 if lcApiKey == "" && lcHost != "" { 301 return vcnerr.ErrNoLcApiKeyEnv 302 } 303 304 //check if an lcUser is present inside the context 305 var lcUser *api.LcUser 306 uif, err := api.GetUserFromContext(store.Config().CurrentContext, lcApiKey, "", signingPubKey) 307 if err != nil { 308 return err 309 } 310 if lctmp, ok := uif.(*api.LcUser); ok { 311 lcUser = lctmp 312 } 313 314 // It uses flags for CNC context client constructor if at least host and apikey are provided 315 if lcHost != "" && lcApiKey != "" { 316 // client from context could be override by the one created from local flags 317 lcUser, err = api.NewLcUser(lcApiKey, "", lcHost, lcPort, lcCert, skipTlsVerify, noTls, signingPubKey) 318 if err != nil { 319 return err 320 } // Store the new config 321 if err := store.SaveConfig(); err != nil { 322 return err 323 } 324 } 325 326 // any set `--bom-xxx` option implies bom mode 327 bomFlag := viper.GetBool("bom") || 328 viper.IsSet("bom-file") || 329 viper.IsSet("bom-signerID") || 330 viper.IsSet("bom-deps-only") || 331 viper.IsSet("bom-spdx") || 332 viper.IsSet("bom-cyclonedx-json") || 333 viper.IsSet("bom-cyclonedx-xml") || 334 viper.IsSet("bom-container-binary") || 335 viper.IsSet("bom-container-hash") || 336 viper.IsSet("bom-batch-size") 337 338 artifacts := make([]*api.Artifact, 0, 1) 339 if lcUser != nil { 340 err = lcUser.Client.Connect() 341 if err != nil { 342 return err 343 } 344 345 for attr := range metadata { 346 if attr == "allowdownload" { 347 err := lcUser.RequireFeatOrErr(schema.FeatAllowDownload) 348 if err != nil { 349 return err 350 } 351 break 352 } 353 } 354 355 if !skipLocalPubKeyComp { 356 err = lcUser.CheckConnectionPublicKey(enforceSignatureVerify) 357 if err != nil { 358 return err 359 } 360 } 361 362 var bomText string 363 bomFile := viper.GetString("bom-file") 364 365 if bomFlag { 366 err := lcUser.RequireFeatOrErr(schema.FeatBoM) 367 if err != nil { 368 return err 369 } 370 } 371 outputOpts := artifact.Progress 372 if viper.GetBool("silent") || output != "" { 373 outputOpts = artifact.Silent 374 } else if viper.GetBool("bom-debug") { 375 outputOpts = artifact.Debug 376 } 377 378 var bomArtifact artifact.Artifact 379 if bomFlag && !viper.IsSet("bom-hashes") { 380 // if bom-file specified, use BoM data from file, otherwise resolve dependencies 381 if bomFile == "" { 382 if len(args) != 1 { 383 return fmt.Errorf("--bom option can be used only with single asset") 384 } 385 path := args[0] 386 u, err := uri.Parse(path) 387 if err != nil { 388 return err 389 } 390 if _, ok := bom.BomSchemes[u.Scheme]; !ok { 391 return fmt.Errorf("unsupported URI %s for --bom option", path) 392 } 393 if u.Scheme != "" { 394 path = strings.TrimPrefix(u.Opaque, "//") 395 } 396 if u.Scheme == "docker" { 397 binaries := viper.GetStringSlice("bom-container-binary") 398 dockerArtifact, err := docker.New(path, binaries) 399 if err != nil { 400 return err 401 } 402 if !viper.GetBool("bom-container-hash") && len(binaries) > 0 { 403 // use binary hash rather than container hash 404 if len(binaries) != 1 { 405 return errors.New("cannot use binary hash when several binaries are specified. Use --bom-container-hash option to use container hash") 406 } 407 // setting hash disables hash calculation from container image 408 hash, err = dockerArtifact.FileHash(binaries[0]) 409 if err != nil { 410 return err 411 } 412 name = filepath.Base(binaries[0]) 413 } 414 bomArtifact = dockerArtifact 415 } else { 416 path, err = filepath.Abs(path) 417 if err != nil { 418 return err 419 } 420 bomArtifact = bom.New(path) 421 } 422 if bomArtifact == nil { 423 return fmt.Errorf("unsupported asset format/language") 424 } 425 } else { 426 bomArtifact, err = artifact.Load(bomFile) 427 if err != nil { 428 return fmt.Errorf("cannot load BoM from file: %w", err) 429 } 430 } 431 432 if outputOpts != artifact.Silent { 433 fmt.Printf("Resolving dependencies...\n") 434 } 435 deps, err := bomArtifact.ResolveDependencies(outputOpts) 436 if err != nil { 437 return fmt.Errorf("cannot get dependencies: %w", err) 438 } 439 440 bomBatchSize := int(viper.GetUint("bom-batch-size")) 441 442 bomText, err = notarizeDeps(lcUser, deps, outputOpts, bomArtifact.Type(), bomBatchSize) 443 if err != nil { 444 return err 445 } 446 447 if bomFile != "" { 448 // just to keep BoM file current 449 err = artifact.Store(bomArtifact, bomFile) 450 if err != nil { 451 // show warning, but not error, because notarization succeeded 452 fmt.Printf("Cannot store actual BoM: %v", err) 453 } 454 } 455 456 err = bom.Output(bomArtifact) // process all possible BoM output options 457 if err != nil { 458 // show warning, but not error, because authentication finished 459 fmt.Println(err) 460 } 461 if outputOpts != artifact.Silent { 462 artifact.Display(bomArtifact, artifact.ColNameVersion|artifact.ColHash|artifact.ColTrustLevel) 463 } 464 } 465 466 // Dependencies specified as hashes, not resolved 467 if viper.IsSet("bom-hashes") { 468 // TODO: Is it correct that BoM output is not allowed for hashes? 469 if viper.IsSet("bom-deps-only") || 470 viper.IsSet("bom-spdx") || 471 viper.IsSet("bom-cyclonedx-json") || 472 viper.IsSet("bom-cyclonedx-xml") { 473 return fmt.Errorf("bom-hashes option cannot be combined with bom-deps-only or any BoM output option") 474 } 475 hashes := viper.GetStringSlice("bom-hashes") 476 477 signerID := viper.GetString("bom-signerID") 478 if signerID == "" { 479 signerID = api.GetSignerIDByApiKey(lcUser.Client.ApiKey) 480 } 481 482 bomText, err = bomHashes(lcUser, signerID, output, hashes) 483 if err != nil { 484 return err 485 } 486 } 487 488 // notarize the asset if not instructed otherwise 489 if !viper.GetBool("bom-deps-only") { 490 if hash != "" { 491 hash = strings.ToLower(hash) 492 // Load existing artifact, if any, otherwise use an empty artifact 493 if ar, _, err := lcUser.LoadArtifact(hash, "", "", 0, nil); err == nil && ar != nil { 494 artifacts = append(artifacts, &api.Artifact{ 495 Kind: ar.Kind, 496 Name: ar.Name, 497 Hash: ar.Hash, 498 Size: ar.Size, 499 ContentType: ar.ContentType, 500 Metadata: ar.Metadata, 501 }) 502 } else { 503 if name == "" { 504 return fmt.Errorf("please set an asset name, by using --name") 505 } 506 artifacts = append(artifacts, &api.Artifact{Hash: hash}) 507 } 508 } else { 509 artifacts, err = extractor.Extract(args, extractorOptions...) 510 if err != nil { 511 return err 512 } 513 } 514 err = LcSign(lcUser, artifacts, state, output, name, metadata, attachments, lcVerbose, bomText) 515 if err != nil { 516 return err 517 } 518 } 519 520 // cascade processing 521 if viper.GetBool("bom-cascade") && (state == meta.StatusUntrusted || state == meta.StatusUnsupported || state == meta.StatusTrusted) { 522 err = bomCascade(lcUser, artifacts, output, state, lcVerbose) 523 if err != nil { 524 return err 525 } 526 } 527 528 return nil 529 } 530 531 // User 532 if err := assert.UserLogin(); err != nil { 533 return err 534 } 535 u, ok := uif.(*api.User) 536 if !ok { 537 return fmt.Errorf("cannot load the current user") 538 } 539 540 // Make the artifact to be signed 541 542 if hash != "" { 543 if alert != nil { 544 return fmt.Errorf("cannot use --create-alert with --hash") 545 } 546 hash = strings.ToLower(hash) 547 // Load existing artifact, if any, otherwise use an empty artifact 548 if ar, err := u.LoadArtifact(hash); err == nil && ar != nil { 549 artifacts = []*api.Artifact{ar.Artifact()} 550 } else { 551 if name == "" { 552 return fmt.Errorf("please set an asset name, by using --name") 553 } 554 artifacts = []*api.Artifact{{Hash: hash}} 555 } 556 } else { 557 // Extract artifact from arg 558 artifacts, err = extractor.Extract(args, extractorOptions...) 559 if err != nil { 560 return err 561 } 562 } 563 564 if artifacts == nil { 565 return fmt.Errorf("unable to process the input asset provided") 566 } 567 568 if len(artifacts) == 1 { 569 // Override the asset's name, if provided by --name 570 if name != "" { 571 artifacts[0].Name = name 572 } 573 } 574 575 for _, a := range artifacts { 576 // Copy user provided custom attributes 577 a.Metadata.SetValues(metadata) 578 579 err := sign(*u, *a, state, meta.VisibilityForFlag(public), output, silentMode, readOnly, alert) 580 if err != nil { 581 return err 582 } 583 } 584 585 return nil 586 } 587 588 func sign(u api.User, a api.Artifact, state meta.Status, visibility meta.Visibility, output string, silent bool, readOnly bool, alert *alertOptions) error { 589 590 if output == "" { 591 color.Set(meta.StyleAffordance()) 592 fmt.Print("Your assets will not be uploaded. They will be processed locally.") 593 color.Unset() 594 fmt.Println() 595 fmt.Println() 596 fmt.Println("Signer:\t" + u.Email()) 597 } 598 599 hook := newHook(&a) 600 601 s := spin.New("%s Notarization in progress...") 602 s.Set(spin.Spin1) 603 604 var verification *api.BlockchainVerification 605 var err error 606 607 for i := 1; true; i++ { 608 var passphrase string 609 var interactive bool 610 passphrase, interactive, err = cli.ProvidePassphrase() 611 if err != nil { 612 return err 613 } 614 615 if output == "" && !silent { 616 s.Start() 617 } 618 619 var keyin string 620 var offline bool 621 keyin, _, offline, err = u.Secret() 622 if err != nil { 623 return err 624 } 625 if offline { 626 return fmt.Errorf("offline secret is not supported by the current vcn version") 627 } 628 629 verification, err = u.Sign( 630 a, 631 api.SignWithStatus(state), 632 api.SignWithVisibility(visibility), 633 api.SignWithKey(keyin, passphrase), 634 ) 635 636 if err != nil && i >= 3 { 637 s.Stop() 638 return fmt.Errorf("too many failed attempts: %s", err) 639 } 640 641 if interactive && err == api.WrongPassphraseErr { 642 s.Stop() 643 fmt.Printf("\nError: %s, please try again\n\n", err.Error()) 644 continue 645 } 646 break 647 } 648 649 s.Stop() 650 651 if err != nil { 652 return err 653 } 654 655 // once transaction is confirmed we don't want to show errors, just print warnings instead. 656 657 // todo(ameingast/leogr): remove redundant event - need backend improvement 658 api.TrackPublisher(&u, meta.VcnSignEvent) 659 api.TrackSign(&u, a.Hash, a.Name, state) 660 661 err = hook.finalize(verification, readOnly) 662 if err != nil { 663 return cli.PrintWarning(output, err.Error()) 664 } 665 666 if output == "" { 667 fmt.Println() 668 } 669 670 artifact, err := api.LoadArtifact(&u, a.Hash, verification.MetaHash()) 671 if err != nil { 672 return cli.PrintWarning(output, err.Error()) 673 } 674 675 cli.Print(output, types.NewResult(&a, artifact, verification)) 676 677 if alert != nil { 678 if err := handleAlert(alert, u, a, *verification, output); err != nil { 679 return cli.PrintWarning(output, err.Error()) 680 } 681 } 682 683 return nil 684 } 685 686 func pipeMode() bool { 687 fileInfo, _ := os.Stdin.Stat() 688 return fileInfo.Mode()&os.ModeCharDevice == 0 689 } 690 691 func notarizeDeps(lcUser *api.LcUser, deps []artifact.Dependency, outputOpts artifact.OutputOptions, artType string, batchSize int) (string, error) { 692 if outputOpts != artifact.Silent { 693 fmt.Printf("Authenticating dependencies...\n") 694 } 695 696 signerID := viper.GetString("bom-signerID") 697 if signerID == "" { 698 signerID = api.GetSignerIDByApiKey(lcUser.Client.ApiKey) 699 } 700 701 force := viper.GetBool("bom-force") 702 703 var bar *progressbar.ProgressBar 704 if len(deps) > 1 && outputOpts == artifact.Progress { 705 bar = progressbar.Default(int64(len(deps))) 706 } 707 708 progressCallback := func(processedDeps []artifact.Dependency) { 709 switch outputOpts { 710 case artifact.Progress: 711 if bar != nil { 712 bar.Add(len(processedDeps)) 713 } 714 case artifact.Debug: 715 for _, d := range processedDeps { 716 fmt.Printf("%s@%s (%s) - %s\n", d.Name, d.Version, d.Hash, artifact.TrustLevelName(d.TrustLevel)) 717 } 718 } 719 } 720 721 errs, err := artifact.AuthenticateDependencies(lcUser, signerID, deps, batchSize, progressCallback) 722 if err != nil { 723 return "", fmt.Errorf("error authenticating dependencies: %w", err) 724 } 725 726 var msgs []string 727 var depsToNotarize []*artifact.Dependency 728 var kinds []string 729 730 for i := range deps { // Authenticate mutates the dependency, so use the index 731 if errs[i] != nil { 732 return "", fmt.Errorf("cannot authenticate %s@%s dependency: %w", 733 deps[i].SignerID, deps[i].Version, errs[i]) 734 } 735 if deps[i].TrustLevel < artifact.Unknown { 736 msgs = append(msgs, fmt.Sprintf("Dependency %s@%s trust level is %s", 737 deps[i].Name, deps[i].Version, artifact.TrustLevelName(deps[i].TrustLevel))) 738 } 739 if deps[i].TrustLevel < artifact.Trusted { 740 depsToNotarize = append(depsToNotarize, &deps[i]) 741 kinds = append(kinds, artType) 742 } 743 } 744 745 if len(msgs) > 0 && !force { 746 for _, msg := range msgs { 747 fmt.Println(msg) 748 } 749 return "", fmt.Errorf("some dependencies have insufficient trust level and cannot be automatically notarized. You can override it with --bom-force option") 750 } 751 752 // notarize only the dependencies first to make sure all needed keys are present into DB before 753 // adding key references to the index 754 // we don't get here if 'force' flag isn't set and some dependencies are untrusted 755 if len(depsToNotarize) > 0 { 756 var bar *progressbar.ProgressBar 757 if outputOpts != artifact.Silent { 758 ds := "dependencies" 759 if len(depsToNotarize) == 1 { 760 ds = "dependency" 761 } 762 fmt.Printf("Notarizing %d %s ...\n", len(depsToNotarize), ds) 763 if outputOpts == artifact.Progress { 764 bar = progressbar.Default(int64(len(depsToNotarize))) 765 } 766 } 767 768 progressCallbackN := func(processedDeps []*artifact.Dependency) { 769 switch outputOpts { 770 case artifact.Progress: 771 if bar != nil { 772 bar.Add(len(processedDeps)) 773 } 774 case artifact.Debug: 775 for _, d := range processedDeps { 776 fmt.Printf("%s@%s (%s)\n", d.Name, d.Version, d.Hash) 777 } 778 } 779 } 780 781 errs, err = artifact.NotarizeDependencies(lcUser, kinds, depsToNotarize, batchSize, progressCallbackN) 782 if err != nil { 783 return "", fmt.Errorf("error notarizing dependencies: %w", err) 784 } 785 var errMsgs []string 786 for _, e := range errs { 787 if e != nil { 788 errMsgs = append(errMsgs, e.Error()) 789 } 790 } 791 if len(errMsgs) > 0 { 792 return "", fmt.Errorf("error notarizing (some) dependencies: %s", strings.Join(errMsgs, ", ")) 793 } 794 } else { 795 fmt.Printf("No dependencies require notarization\n") 796 } 797 798 var builder strings.Builder 799 for i := range deps { 800 // add dep key to BoM list for attaching 801 fmt.Fprintf(&builder, "vcn.%s.%s\n", deps[i].SignerID, deps[i].Hash) 802 } 803 804 return builder.String(), nil 805 } 806 807 func bomHashes(lcUser *api.LcUser, signerID string, output string, hashes []string) (string, error) { 808 var builder strings.Builder 809 if output == "" { 810 fmt.Println("Resolving hashes...") 811 } 812 for _, hash := range hashes { 813 ar, verified, err := lcUser.LoadArtifact(hash, signerID, "", 0, nil) 814 if err == api.ErrNotFound || ar.Status != meta.StatusTrusted { 815 return "", fmt.Errorf("you can only add BoM hashes of known trusted artifacts (please notarize the artifact using vcn notarize) before adding it to --bom-hashes") 816 } 817 if err != nil { 818 return "", err 819 } 820 if !verified { 821 return "", fmt.Errorf("the ledger is compromised") 822 } 823 if output == "" { 824 fmt.Printf("%s ==> %s", hash, ar.Name) 825 ver, ok := ar.Metadata.Get("version", "").(string) 826 if ok && ver != "" { 827 fmt.Printf("@%s", ver) 828 } 829 fmt.Println() 830 } 831 832 fmt.Fprintf(&builder, "vcn.%s.%s\n", signerID, hash) 833 } 834 return builder.String(), nil 835 } 836 837 func bomCascade(lcUser *api.LcUser, artifacts []*api.Artifact, output string, state meta.Status, lcVerbose bool) error { 838 signerID := api.GetSignerIDByApiKey(lcUser.Client.ApiKey) 839 toProcess := make([]api.PackageDetails, 0) 840 for _, ar := range artifacts { 841 included, err := verify.GetIncluded(ar.Hash, signerID, lcUser) 842 if err != nil { 843 return fmt.Errorf("error finding assets for %s: %w", ar.Name, err) 844 } 845 toProcess = append(toProcess, included...) 846 } 847 cascaded := make([]*api.Artifact, 0) 848 for _, asset := range toProcess { 849 if asset.Status == state { 850 continue 851 } 852 ar, _, err := lcUser.LoadArtifact(asset.Hash, "", "", 0, nil) 853 if err != nil { 854 return err 855 } 856 if ar == nil { 857 return fmt.Errorf("cannot resolve artifact by hash %s", asset.Hash) 858 } 859 cascaded = append(cascaded, &api.Artifact{ 860 Kind: ar.Kind, 861 Name: ar.Name, 862 Hash: ar.Hash, 863 Size: ar.Size, 864 ContentType: ar.ContentType, 865 Metadata: ar.Metadata, 866 }) 867 } 868 if len(cascaded) > 0 && !viper.GetBool("bom-force") { 869 if !terminal.IsTerminal(int(os.Stdout.Fd())) { 870 return errors.New("run vcn interactively or specify --bom-force for cascade operations") 871 } 872 fmt.Printf("Following assets depend on assets being processed:\n") 873 for _, asset := range cascaded { 874 fmt.Printf("%s", asset.Name) 875 ver, ok := asset.Metadata.Get("version", "").(string) 876 if ok && ver != "" { 877 fmt.Printf("@%s", ver) 878 } 879 fmt.Printf(" (%s)\n", asset.Hash) 880 } 881 actionMap := map[meta.Status]string{ 882 meta.StatusUntrusted: "untrust", 883 meta.StatusUnsupported: "unsupport", 884 meta.StatusTrusted: "notarize", 885 } 886 for { 887 fmt.Printf("Are you sure you want to %s these assets? (y/N)", actionMap[state]) 888 var confirm string 889 _, err := fmt.Scanln(&confirm) 890 if err != nil { 891 return err 892 } 893 confirm = strings.ToLower(strings.TrimSuffix(confirm, "\n")) 894 if confirm == "y" { 895 break 896 } 897 if confirm == "n" || confirm == "" { 898 return errors.New("cascade operation aborted") 899 } 900 fmt.Println("please enter y or n") 901 } 902 } 903 err := LcSign(lcUser, cascaded, state, output, "", nil, nil, lcVerbose, "") 904 if err != nil { 905 return err 906 } 907 return nil 908 }