diff --git a/internal/commands/scan.go b/internal/commands/scan.go index 7b3771baf..5de5ae9c4 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -3,6 +3,7 @@ package commands import ( "archive/zip" "encoding/json" + "encoding/xml" "fmt" "io" "io/fs" @@ -118,7 +119,11 @@ const ( ScsRepoWarningMsg = "SCS scan warning: Unable to start Scorecard scan due to missing required flags, please include in the ast-cli arguments: " + "--scs-repo-url your_repo_url --scs-repo-token your_repo_token" ScsScorecardUnsupportedHostWarningMsg = "SCS scan warning: Unable to run Scorecard scanner due to unsupported repo host. Currently, Scorecard can only run on GitHub Cloud repos." - BranchPrimaryPrefix = "--branch-primary=" + + jsonExt = ".json" + xmlExt = ".xml" + sbomScanTypeErrMsg = "The --sbom-only flag can only be used when the scan type is sca" + BranchPrimaryPrefix = "--branch-primary=" ) var ( @@ -808,6 +813,9 @@ func scanCreateSubCommand( createScanCmd.PersistentFlags().Bool(commonParams.ContainersExcludeNonFinalStagesFlag, false, "Scan only the final deployable image") createScanCmd.PersistentFlags().String(commonParams.ContainersImageTagFilterFlag, "", "Exclude images by image name and/or tag, ex: \"*dev\"") + // reading sbom-only flag + createScanCmd.PersistentFlags().Bool(commonParams.SbomFlag, false, "Scan only the specified SBOM file (supported formats xml or json)") + return createScanCmd } @@ -1090,6 +1098,7 @@ func addScaScan(cmd *cobra.Command, resubmitConfig []wrappers.Config, hasContain scaMapConfig := make(map[string]interface{}) scaConfig := wrappers.ScaConfig{} scaMapConfig[resultsMapType] = commonParams.ScaType + isSbom, _ := cmd.PersistentFlags().GetBool(commonParams.SbomFlag) scaConfig.Filter, _ = cmd.Flags().GetString(commonParams.ScaFilterFlag) scaConfig.LastSastScanTime, _ = cmd.Flags().GetString(commonParams.LastSastScanTime) scaConfig.PrivatePackageVersion, _ = cmd.Flags().GetString(commonParams.ScaPrivatePackageVersionFlag) @@ -1108,6 +1117,7 @@ func addScaScan(cmd *cobra.Command, resubmitConfig []wrappers.Config, hasContain } } } + scaConfig.SBom = strconv.FormatBool(isSbom) scaMapConfig[resultsMapValue] = &scaConfig return scaMapConfig } @@ -1323,6 +1333,8 @@ func validateScanTypes(cmd *cobra.Command, jwtWrapper wrappers.JWTWrapper, featu var scanTypes []string var SCSScanTypes []string + isSbomScan, _ := cmd.PersistentFlags().GetBool(commonParams.SbomFlag) + allowedEngines, err := jwtWrapper.GetAllowedEngines(featureFlagsWrapper) if err != nil { err = errors.Errorf("Error validating scan types: %v", err) @@ -1338,6 +1350,20 @@ func validateScanTypes(cmd *cobra.Command, jwtWrapper wrappers.JWTWrapper, featu userSCSScanTypes = strings.Replace(strings.ToLower(userSCSScanTypes), commonParams.SCSEnginesFlag, commonParams.ScsType, 1) scanTypes = strings.Split(userScanTypes, ",") + + // check scan-types, when sbom-only flag is used + if isSbomScan { + if len(scanTypes) > 1 { + err = errors.Errorf(sbomScanTypeErrMsg) + return err + } + + if scanTypes[0] != "sca" { + err = errors.Errorf(sbomScanTypeErrMsg) + return err + } + } + for _, scanType := range scanTypes { if !allowedEngines[scanType] { keys := reflect.ValueOf(allowedEngines).MapKeys() @@ -1353,11 +1379,19 @@ func validateScanTypes(cmd *cobra.Command, jwtWrapper wrappers.JWTWrapper, featu return err } } else { - for k := range allowedEngines { - scanTypes = append(scanTypes, k) + if isSbomScan { + if allowedEngines["sca"] { + // for sbom-flag, setting scan-type as only "sca" + scanTypes = append(scanTypes, "sca") + } else { + return errors.Errorf("sbom needs sca engine to be allowed") + } + } else { + for k := range allowedEngines { + scanTypes = append(scanTypes, k) + } } } - actualScanTypes = strings.Join(scanTypes, ",") actualScanTypes = strings.Replace(strings.ToLower(actualScanTypes), commonParams.IacType, commonParams.KicsType, 1) @@ -1656,8 +1690,24 @@ func getUploadURLFromSource(cmd *cobra.Command, uploadsWrapper wrappers.UploadsW scaResolverPath, _ := cmd.Flags().GetString(commonParams.ScaResolverFlag) scaResolverParams, scaResolver := getScaResolverFlags(cmd) - - zipFilePath, directoryPath, err := definePathForZipFileOrDirectory(cmd) + isSbom, _ := cmd.PersistentFlags().GetBool(commonParams.SbomFlag) + var directoryPath string + if isSbom { + sbomFile, _ := cmd.Flags().GetString(commonParams.SourcesFlag) + isValid, err := isValidJSONOrXML(sbomFile) + if err != nil { + return "", "", errors.Wrapf(err, "%s: Input in bad format", failedCreating) + } + if !isValid { + return "", "", errors.Wrapf(err, "%s: Input in bad format", failedCreating) + } + zipFilePath, err = util.CompressFile(sbomFile, "sbomFileCompress", directoryCreationPrefix) + if err != nil { + return "", "", errors.Wrapf(err, "%s: Input in bad format", failedCreating) + } + } else { + zipFilePath, directoryPath, err = definePathForZipFileOrDirectory(cmd) + } if zipFilePath != "" && scaResolverPath != "" { return "", "", errors.New("Scanning Zip files is not supported by ScaResolver.Please use non-zip source") @@ -1717,7 +1767,9 @@ func getUploadURLFromSource(cmd *cobra.Command, uploadsWrapper wrappers.UploadsW } } } else { - zipFilePath, dirPathErr = compressFolder(directoryPath, sourceDirFilter, userIncludeFilter, scaResolver) + if !isSbom { + zipFilePath, dirPathErr = compressFolder(directoryPath, sourceDirFilter, userIncludeFilter, scaResolver) + } } if dirPathErr != nil { return "", "", dirPathErr @@ -1731,8 +1783,10 @@ func getUploadURLFromSource(cmd *cobra.Command, uploadsWrapper wrappers.UploadsW } } - if zipFilePath != "" { + if zipFilePath != "" && !isSbom { return uploadZip(uploadsWrapper, zipFilePath, unzip, userProvidedZip, featureFlagsWrapper) + } else if zipFilePath != "" && isSbom { + return uploadZip(uploadsWrapper, zipFilePath, unzip, false, featureFlagsWrapper) } return preSignedURL, zipFilePath, nil } @@ -2978,8 +3032,9 @@ func deprecatedFlagValue(cmd *cobra.Command, deprecatedFlagKey, inUseFlagKey str } func validateCreateScanFlags(cmd *cobra.Command) error { + isSbomScan, _ := cmd.PersistentFlags().GetBool(commonParams.SbomFlag) branch := strings.TrimSpace(viper.GetString(commonParams.BranchKey)) - if branch == "" { + if branch == "" && !isSbomScan { return errors.Errorf("%s: Please provide a branch", failedCreating) } exploitablePath, _ := cmd.Flags().GetString(commonParams.ExploitablePathFlag) @@ -3109,3 +3164,32 @@ func createMinimalZipFile() (string, error) { return outputFile.Name(), nil } + +func isValidJSONOrXML(path string) (bool, error) { + ext := strings.ToLower(filepath.Ext(path)) + if ext != jsonExt && ext != xmlExt { + return false, fmt.Errorf("not a JSON/XML file, provide valid JSON/XMl file") + } + + data, err := ioutil.ReadFile(path) + if err != nil { + return false, fmt.Errorf("failed to read file: %w", err) + } + + switch ext { + case jsonExt: + var js interface{} + if err := json.Unmarshal(data, &js); err != nil { + return false, fmt.Errorf("invalid JSON format. %w", err) // Invalid JSON + } + case xmlExt: + var x interface{} + if err := xml.Unmarshal(data, &x); err != nil { + return false, fmt.Errorf("invalid XML format.%w", err) // Invalid XML + } + default: + return false, nil + } + + return true, nil +} diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index 4499e3c61..b1efe06b9 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -787,6 +787,7 @@ func TestAddScaScan(t *testing.T) { ExploitablePath: "true", LastSastScanTime: "1", PrivatePackageVersion: "1.1.1", + SBom: "false", } scaMapConfig := make(map[string]interface{}) scaMapConfig[resultsMapType] = commonParams.ScaType @@ -2412,3 +2413,32 @@ func Test_parseArgs(t *testing.T) { } } } + +func Test_isValidJSONOrXML(t *testing.T) { + tests := []struct { + description string + inputPath string + output bool + }{ + {"wrong extension", "somefile.txt", false}, + {"wrong json file", "wrongfilepath.json", false}, + {"wrong xml file", "wrongfilepath.xml", false}, + {"correct file", "data/package.json", true}, + } + + for _, test := range tests { + isValid, _ := isValidJSONOrXML(test.inputPath) + if isValid != test.output { + t.Errorf(" test case failed for params %v", test) + } + } +} + +func Test_CreateScanWithSbomFlag(t *testing.T) { + err := execCmdNotNilAssertion( + t, + "scan", "create", "--project-name", "newProject", "-s", "data/sbom.json", "--branch", "dummy_branch", "--sbom-only", + ) + + assert.ErrorContains(t, err, "Failed creating a scan: Input in bad format: failed to read file:") +} diff --git a/internal/params/flags.go b/internal/params/flags.go index 365f756cf..56afc9df1 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -231,6 +231,9 @@ const ( ContainersImageTagFilterFlag = "containers-image-tag-filter" ContainersPackageFilterFlag = "containers-package-filter" ContainersExcludeNonFinalStagesFlag = "containers-exclude-non-final-stages" + + // SBOM - flag + SbomFlag = "sbom-only" ) // Parameter values diff --git a/internal/wrappers/scans.go b/internal/wrappers/scans.go index 07f1ddda5..b1dc5a028 100644 --- a/internal/wrappers/scans.go +++ b/internal/wrappers/scans.go @@ -143,6 +143,7 @@ type ScaConfig struct { LastSastScanTime string `json:"LastSastScanTime,omitempty"` PrivatePackageVersion string `json:"privatePackageVersion,omitempty"` EnableContainersScan bool `json:"enableContainersScan,omitempty"` + SBom string `json:"sbom,omitempty"` } type ContainerConfig struct { FilesFilter string `json:"filesFilter,omitempty"` diff --git a/test/integration/scan_test.go b/test/integration/scan_test.go index dbabe7c8c..dbcae65be 100644 --- a/test/integration/scan_test.go +++ b/test/integration/scan_test.go @@ -2383,3 +2383,48 @@ func TestCreateScan_WithScaResolver_ZipSource_Fail(t *testing.T) { err, _ := executeCommand(t, args...) assert.Error(t, err, "Scanning Zip files is not supported by ScaResolver.Please use non-zip source") } + +func TestCreateScan_SbomScanForInvalidScanTypes(t *testing.T) { + args := []string{ + "scan", "create", + flag(params.ProjectName), "random_proj", + flag(params.SourcesFlag), "data/project-with-directory-symlink", + flag(params.ScanTypes), "sast,sca", + flag(params.BranchFlag), "dummy_branch", + flag(params.SbomFlag), + } + + err, _ := executeCommand(t, args...) + assert.Error(t, err, "The --sbom-only flag can only be used when the scan type is sca") + +} + +func TestCreateScan_SbomScanForInvalidFileExtension(t *testing.T) { + args := []string{ + "scan", "create", + flag(params.ProjectName), "random_proj", + flag(params.SourcesFlag), "data/project-with-directory-symlink", + flag(params.ScanTypes), "sca", + flag(params.BranchFlag), "dummy_branch", + flag(params.SbomFlag), + } + + err, _ := executeCommand(t, args...) + assert.Error(t, err, "Failed creating a scan: Input in bad format: not a JSON/XML file, provide valid JSON/XMl file") + +} + +func TestCreateScan_SbomScanForNotExistingFile(t *testing.T) { + args := []string{ + "scan", "create", + flag(params.ProjectName), "random_proj", + flag(params.SourcesFlag), "data/sbom.json", + flag(params.ScanTypes), "sca", + flag(params.BranchFlag), "dummy_branch", + flag(params.SbomFlag), + } + + err, _ := executeCommand(t, args...) + assert.ErrorContains(t, err, "Failed creating a scan: Input in bad format: failed to read file:") + +}