diff --git a/cmd/export.go b/cmd/export.go index a5cfa5b1..146ef0de 100644 --- a/cmd/export.go +++ b/cmd/export.go @@ -31,7 +31,6 @@ import ( "github.com/protobom/protobom/pkg/native/serializers" "github.com/protobom/protobom/pkg/writer" "github.com/spf13/cobra" - "github.com/spf13/viper" "github.com/bomctl/bomctl/internal/pkg/export" "github.com/bomctl/bomctl/internal/pkg/options" @@ -182,21 +181,6 @@ func parseFormat(fs, encoding string) (formats.Format, error) { return format, nil } -func preRun(opts *options.Options) func(*cobra.Command, []string) { - return func(cmd *cobra.Command, _ []string) { - cfgFile, err := cmd.Flags().GetString("config") - cobra.CheckErr(err) - - verbosity, err := cmd.Flags().GetCount("verbose") - cobra.CheckErr(err) - - opts. - WithCacheDir(viper.GetString("cache_dir")). - WithConfigFile(cfgFile). - WithDebug(verbosity >= minDebugLevel) - } -} - func validateEncoding(fs, encoding string) error { if !slices.Contains(encodingOptions()[fs], encoding) { return errEncodingNotSupported diff --git a/cmd/fetch.go b/cmd/fetch.go index 1bb40cdd..f480cf72 100644 --- a/cmd/fetch.go +++ b/cmd/fetch.go @@ -22,7 +22,6 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" "github.com/bomctl/bomctl/internal/pkg/fetch" "github.com/bomctl/bomctl/internal/pkg/options" @@ -35,30 +34,16 @@ func fetchCmd() *cobra.Command { } outputFile := outputFileValue("") - sbomURLs := urlSliceValue{} fetchCmd := &cobra.Command{ - Use: "fetch [flags] SBOM_URL...", - Args: cobra.MinimumNArgs(1), - Short: "Fetch SBOM file(s) from HTTP(S), OCI, or Git URLs", - Long: "Fetch SBOM file(s) from HTTP(S), OCI, or Git URLs", - PreRun: func(_ *cobra.Command, args []string) { - sbomURLs = append(sbomURLs, args...) - }, - Run: func(cmd *cobra.Command, _ []string) { - cfgFile, err := cmd.Flags().GetString("config") - cobra.CheckErr(err) - - verbosity, err := cmd.Flags().GetCount("verbose") - cobra.CheckErr(err) - - opts. - WithCacheDir(viper.GetString("cache_dir")). - WithConfigFile(cfgFile). - WithDebug(verbosity >= minDebugLevel) - + Use: "fetch [flags] SBOM_URL...", + Args: cobra.MinimumNArgs(1), + Short: "Fetch SBOM file(s) from HTTP(S), OCI, or Git URLs", + Long: "Fetch SBOM file(s) from HTTP(S), OCI, or Git URLs", + PreRun: preRun(opts.Options), + Run: func(_ *cobra.Command, args []string) { if string(outputFile) != "" { - if len(sbomURLs) > 1 { + if len(args) > 1 { opts.Logger.Fatal("The --output-file option cannot be used when more than one URL is provided.") } @@ -72,7 +57,7 @@ func fetchCmd() *cobra.Command { defer opts.OutputFile.Close() } - for _, url := range sbomURLs { + for _, url := range args { if err := fetch.Fetch(url, opts); err != nil { opts.Logger.Fatal(err) } diff --git a/cmd/import.go b/cmd/import.go new file mode 100644 index 00000000..ce609ee9 --- /dev/null +++ b/cmd/import.go @@ -0,0 +1,70 @@ +// ------------------------------------------------------------------------ +// SPDX-FileCopyrightText: Copyright © 2024 bomctl a Series of LF Projects, LLC +// SPDX-FileName: cmd/import.go +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: Apache-2.0 +// ------------------------------------------------------------------------ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ +package cmd + +import ( + "os" + "slices" + + "github.com/spf13/cobra" + + imprt "github.com/bomctl/bomctl/internal/pkg/import" + "github.com/bomctl/bomctl/internal/pkg/options" + "github.com/bomctl/bomctl/internal/pkg/utils" +) + +func importCmd() *cobra.Command { + opts := &imprt.ImportOptions{ + Options: options.New(options.WithLogger(utils.NewLogger("import"))), + } + + importCmd := &cobra.Command{ + Use: "import [flags] { - | FILE...}", + Args: cobra.MinimumNArgs(1), + Short: "Import SBOM file(s) from stdin or local filesystem", + Long: "Import SBOM file(s) from stdin or local filesystem", + PreRun: preRun(opts.Options), + Run: func(_ *cobra.Command, args []string) { + if slices.Contains(args, "-") && len(args) > 1 { + opts.Logger.Fatal("Piped input and file path args cannot be specified simultaneously.") + } + + for idx := range args { + if args[idx] == "-" { + opts.InputFiles = append(opts.InputFiles, os.Stdin) + } else { + file, err := os.Open(args[idx]) + if err != nil { + opts.Logger.Fatal("failed to open input file", "err", err, "file", file) + } + + opts.InputFiles = append(opts.InputFiles, file) + + defer file.Close() //nolint:revive + } + } + + if err := imprt.Import(opts); err != nil { + opts.Logger.Fatal(err) + } + }, + } + + return importCmd +} diff --git a/cmd/list.go b/cmd/list.go index 35ae5c2c..b24f28a3 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -52,7 +52,7 @@ func listCmd() *cobra.Command { documentIDs := []string{} listCmd := &cobra.Command{ - Use: "list", + Use: "list [flags] SBOM_ID...", Aliases: []string{"ls"}, Short: "List SBOM documents in local cache", Long: "List SBOM documents in local cache", diff --git a/cmd/root.go b/cmd/root.go index 86e9c8bd..93f271ab 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,6 +26,8 @@ import ( "github.com/charmbracelet/log" "github.com/spf13/cobra" "github.com/spf13/viper" + + "github.com/bomctl/bomctl/internal/pkg/options" ) const ( @@ -111,12 +113,28 @@ func rootCmd() *cobra.Command { rootCmd.AddCommand(exportCmd()) rootCmd.AddCommand(fetchCmd()) + rootCmd.AddCommand(importCmd()) rootCmd.AddCommand(listCmd()) rootCmd.AddCommand(versionCmd()) return rootCmd } +func preRun(opts *options.Options) func(*cobra.Command, []string) { + return func(cmd *cobra.Command, _ []string) { + cfgFile, err := cmd.Flags().GetString("config") + cobra.CheckErr(err) + + verbosity, err := cmd.Flags().GetCount("verbose") + cobra.CheckErr(err) + + opts. + WithCacheDir(viper.GetString("cache_dir")). + WithConfigFile(cfgFile). + WithDebug(verbosity >= minDebugLevel) + } +} + func Execute() { cobra.CheckErr(rootCmd().Execute()) } diff --git a/go.mod b/go.mod index 1d1fff0c..00a550f5 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/protobom/protobom v0.4.3 github.com/protobom/storage v0.1.3 github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 oras.land/oras-go/v2 v2.5.0 @@ -72,7 +73,6 @@ require ( github.com/spdx/tools-golang v0.5.4 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zclconf/go-cty v1.8.0 // indirect diff --git a/internal/pkg/import/import.go b/internal/pkg/import/import.go new file mode 100644 index 00000000..9f76d61c --- /dev/null +++ b/internal/pkg/import/import.go @@ -0,0 +1,70 @@ +// ------------------------------------------------------------------------ +// SPDX-FileCopyrightText: Copyright © 2024 bomctl a Series of LF Projects, LLC +// SPDX-FileName: internal/pkg/import/import.go +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: Apache-2.0 +// ------------------------------------------------------------------------ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ +package imprt + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/protobom/protobom/pkg/reader" + + "github.com/bomctl/bomctl/internal/pkg/db" + "github.com/bomctl/bomctl/internal/pkg/options" +) + +type ImportOptions struct { + *options.Options + InputFiles []*os.File +} + +func Import(opts *ImportOptions) error { + backend := db.NewBackend(). + Debug(opts.Debug). + WithDatabaseFile(filepath.Join(opts.CacheDir, db.DatabaseFile)). + WithLogger(opts.Logger) + + if err := backend.InitClient(); err != nil { + return fmt.Errorf("failed to initialize backend client: %w", err) + } + + defer backend.CloseClient() + + sbomReader := reader.New() + + for idx := range opts.InputFiles { + data, err := io.ReadAll(opts.InputFiles[idx]) + if err != nil { + return fmt.Errorf("failed to read from %s: %w", opts.InputFiles[idx].Name(), err) + } + + document, err := sbomReader.ParseStream(bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("failed to parse %s: %w", opts.InputFiles[idx].Name(), err) + } + + if err := backend.AddDocument(document); err != nil { + return fmt.Errorf("failed to store document: %w", err) + } + } + + return nil +}