diff --git a/client/sbom.go b/client/sbom.go new file mode 100644 index 0000000..dfd74aa --- /dev/null +++ b/client/sbom.go @@ -0,0 +1,120 @@ +package client + +import ( + "bytes" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "os" + "path/filepath" + + "github.com/kondukto-io/kdt/klog" + "github.com/spf13/viper" +) + +func (c *Client) ImportSBOM(file string, repo string, form ImportForm) error { + klog.Debugf("importing sbom content using file:%s", file) + + projectId := form["project"] + + if projectId != "" { + projects, err := c.ListProjects(projectId, repo) + if err != nil { + return err + } + + if len(projects) == 0 { + return errors.New("no projects found for given parameters") + } + + if len(projects) == 1 { + projectId = projects[0].ID + form["project"] = projects[0].Name + } + + if len(projects) > 1 { + return errors.New("multiple projects found for given parameters") + } + } else { + projects, err := c.ListProjects(projectId, repo) + if err != nil { + return err + } + + if len(projects) == 0 { + return errors.New("no projects found for given parameters") + } + + if len(projects) == 1 { + projectId = projects[0].ID + form["project"] = projects[0].Name + } + + if len(projects) > 1 { + return errors.New("multiple projects found for given parameters") + } + } + + path := fmt.Sprintf("/api/v2/%s/sbom/upload", projectId) + rel := &url.URL{Path: path} + u := c.BaseURL.ResolveReference(rel) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + if _, err := os.Stat(file); os.IsNotExist(err) { + return err + } + f, err := os.Open(file) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + part, err := writer.CreateFormFile("file", filepath.Base(f.Name())) + if err != nil { + return err + } + _, err = io.Copy(part, f) + if err != nil { + return err + } + + for k := range form { + if err = writer.WriteField(k, form[k]); err != nil { + return fmt.Errorf("failed to write form field [%s]: %w", k, err) + } + } + + _ = writer.Close() + + req, err := http.NewRequest(http.MethodPost, u.String(), body) + if err != nil { + return err + } + + req.Header.Add("Content-Type", writer.FormDataContentType()) + + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", userAgent) + req.Header.Set("X-Cookie", viper.GetString("token")) + + type importSBOMResponse struct { + Message string `json:"message"` + Error string `json:"error"` + } + + var importResponse importSBOMResponse + resp, err := c.do(req, &importResponse) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to import sbom: %s, status code: %s", importResponse.Error, resp.StatusCode) + } + + return nil +} diff --git a/cmd/sbom.go b/cmd/sbom.go new file mode 100644 index 0000000..887e0fb --- /dev/null +++ b/cmd/sbom.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "errors" + "fmt" + + "github.com/kondukto-io/kdt/client" + "github.com/kondukto-io/kdt/klog" + "github.com/spf13/cobra" +) + +// sbomCmd represents the sbom root command +var sbomCmd = &cobra.Command{ + Use: "sbom", + Short: "base command for SBOM(Software bill of materials) imports", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + _ = cmd.Help() + qwm(ExitCodeSuccess, "") + } + }, +} + +func init() { + rootCmd.AddCommand(sbomCmd) + + sbomCmd.AddCommand(importSbomCmd) + + importSbomCmd.Flags().StringP("file", "f", "", "SBOM file to be imported. Currently only .json format is supported") + importSbomCmd.Flags().StringP("project", "p", "", "Kondukto project id or name") + importSbomCmd.Flags().StringP("repo-id", "r", "", "URL or ID of ALM repository") + importSbomCmd.Flags().StringP("sbom-type", "s", "", "Custom type(optional). Passing a different value than existing type(i.e application, container etc.) is advised") + importSbomCmd.Flags().StringP("branch", "b", "", "Branch name for the project receiving the sbom") +} + +// importSbomCmd represents the sbom import command +var importSbomCmd = &cobra.Command{ + Use: "import", + Short: "imports sbom file to Kondukto", + Run: importSbomRootCommand, +} + +func importSbomRootCommand(cmd *cobra.Command, _ []string) { + // Initialize Kondukto client + c, err := client.New() + if err != nil { + qwe(ExitCodeError, err, "could not initialize Kondukto client") + } + sbomImport := SBOMImport{ + cmd: cmd, + client: c, + } + if err = sbomImport.sbomImport(); err != nil { + qwe(ExitCodeError, err, "failed to import sbom file") + } +} + +type SBOMImport struct { + cmd *cobra.Command + client *client.Client +} + +func (s *SBOMImport) sbomImport() error { + // Parse command line flags needed for file uploads + file, err := s.cmd.Flags().GetString("file") + if err != nil { + return fmt.Errorf("failed to parse file flag: %w", err) + } + + if !s.cmd.Flags().Changed("repo-id") && !s.cmd.Flags().Changed("project") { + return errors.New("missing a required flag(repo or project) to get project detail") + } + + projectName, err := s.cmd.Flags().GetString("project") + if err != nil { + return fmt.Errorf("failed to get project flag: %w", err) + } + + branch, err := s.cmd.Flags().GetString("branch") + if err != nil { + return fmt.Errorf("failed to parse branch flag: %w", err) + } + + repo, err := s.cmd.Flags().GetString("repo-id") + if err != nil { + return fmt.Errorf("failed to parse repo-id flag: %w", err) + } + + sbomType, err := s.cmd.Flags().GetString("sbom-type") + if err != nil { + return fmt.Errorf("failed to parse sbom-type flag: %w", err) + } + + var form = client.ImportForm{ + "project": projectName, + "branch": branch, + "sbom_type": sbomType, + } + + err = s.client.ImportSBOM(file, repo, form) + if err != nil { + return fmt.Errorf("failed to import scan results: %w", err) + } + + importInfo := "" + if projectName == "" { + importInfo = fmt.Sprintf("%s(ALM)", repo) + } else { + importInfo = fmt.Sprintf("%s(kondukto project)", projectName) + } + + klog.Printf("sbom file imported successfully for: [%s]", importInfo) + + return nil +} diff --git a/cmd/scan.go b/cmd/scan.go index 2e32672..866f427 100755 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -55,7 +55,7 @@ func init() { scanCmd.Flags().StringP("file", "f", "", "scan result file") scanCmd.Flags().StringP("branch", "b", "", "branch") scanCmd.Flags().StringP("merge-target", "M", "", "source branch name for pull-request") - scanCmd.Flags().StringP("github-pr-number", "", "", "github pull-request number") + scanCmd.Flags().StringP("pr-number", "", "", "pull-request number. supported alms[github, gitlab, azure, bitbucket]") scanCmd.Flags().Bool("no-decoration", false, "no decoration for pr number") scanCmd.Flags().StringP("image", "I", "", "image to scan with container security products") scanCmd.Flags().StringP("agent", "a", "", "agent name for agent type scanners") @@ -629,12 +629,9 @@ func (s *Scan) startScanByProjectToolAndPRNumber() (string, error) { if err != nil { return "", fmt.Errorf("failed to parse no-decoration flag: %w", err) } - prNumber, err := s.cmd.Flags().GetString("github-pr-number") + prNumber, err := s.cmd.Flags().GetString("pr-number") if err != nil { - return "", fmt.Errorf("failed to parse pr-number flag: %w", err) - } - if prNumber == "" { - return "", errors.New("missing pr-number fields") + return "", fmt.Errorf("failed to get request number: %w", err) } override, err := s.cmd.Flags().GetBool("override") if err != nil { @@ -970,7 +967,7 @@ func getScanMode(cmd *cobra.Command) uint { byBranch := cmd.Flag("merge-target").Changed byForkScan := cmd.Flag("fork-scan").Changed byMerge := cmd.Flag("branch").Changed - byGithubPRNumber := cmd.Flag("github-pr-number").Changed + byPRNumber := cmd.Flag("pr-number").Changed byImage := cmd.Flag("image").Changed byRepo := cmd.Flag("repo-id").Changed byProjectORRepo := byProject || byRepo @@ -979,8 +976,8 @@ func getScanMode(cmd *cobra.Command) uint { byProjectAndTool := byProjectORRepo && byTool && !byPR // byProjectAndToolAndFile := byProjectAndTool && byImportFile byProjectAndToolAndForkScan := byProjectORRepo && byTool && byForkScan && !byPR - byProjectAndToolAndPullRequest := byProjectORRepo && byTool && byPR && !byImportFile // - byProjectAndToolAndPullRequestNumber := byProjectORRepo && byTool && byGithubPRNumber && !byImportFile // + byProjectAndToolAndPullRequestNumber := byProjectORRepo && byTool && byPRNumber && !byImportFile + byProjectAndToolAndPullRequest := byProjectORRepo && byTool && byPR && !byImportFile // mode := func() uint { // sorted by priority