diff --git a/cmd/fortifyExecuteScan.go b/cmd/fortifyExecuteScan.go index 2e5f812f6..2e414a645 100644 --- a/cmd/fortifyExecuteScan.go +++ b/cmd/fortifyExecuteScan.go @@ -386,6 +386,11 @@ func prepareReportData(influx *fortifyExecuteScanInflux) fortify.FortifyReportDa func analyseUnauditedIssues(config fortifyExecuteScanOptions, sys fortify.System, projectVersion *models.ProjectVersion, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string, spotChecksCountByCategory *[]fortify.SpotChecksAuditCount) (int, []*models.ProjectVersionIssueGroup, error) { log.Entry().Info("Analyzing unaudited issues") + + if config.SpotCheckMinimumUnit != "percentage" && config.SpotCheckMinimumUnit != "number" { + return 0, nil, fmt.Errorf("Invalid spotCheckMinimumUnit. Please set it as 'percentage' or 'number'.") + } + reducedFilterSelectorSet := sys.ReduceIssueFilterSelectorSet(issueFilterSelectorSet, []string{"Folder"}, nil) fetchedIssueGroups, err := sys.GetProjectIssuesByIDAndFilterSetGroupedBySelector(projectVersion.ID, "", filterSet.GUID, reducedFilterSelectorSet) if err != nil { @@ -454,6 +459,7 @@ func getSpotIssueCount(config fortifyExecuteScanOptions, sys fortify.System, spo overallDelta := 0 overallIssues := 0 overallIssuesAudited := 0 + for _, issueGroup := range spotCheckCategories { group := "" total := 0 @@ -465,9 +471,12 @@ func getSpotIssueCount(config fortifyExecuteScanOptions, sys fortify.System, spo } flagOutput := "" - if ((total <= config.SpotCheckMinimum || config.SpotCheckMinimum < 0) && audited != total) || (total > config.SpotCheckMinimum && audited < config.SpotCheckMinimum) { - currentDelta := config.SpotCheckMinimum - audited - if config.SpotCheckMinimum < 0 || config.SpotCheckMinimum > total { + minSpotChecksPerCategory := getMinSpotChecksPerCategory(config, total) + log.Entry().Debugf("Minimum spot checks for group %v is %v with audit count %v and total issue count %v", group, minSpotChecksPerCategory, audited, total) + + if ((total <= minSpotChecksPerCategory || minSpotChecksPerCategory < 0) && audited != total) || (total > minSpotChecksPerCategory && audited < minSpotChecksPerCategory) { + currentDelta := minSpotChecksPerCategory - audited + if minSpotChecksPerCategory < 0 || minSpotChecksPerCategory > total { currentDelta = total - audited } if currentDelta > 0 { @@ -494,6 +503,30 @@ func getSpotIssueCount(config fortifyExecuteScanOptions, sys fortify.System, spo return overallDelta } +func getMinSpotChecksPerCategory(config fortifyExecuteScanOptions, totalCount int) int { + if config.SpotCheckMinimumUnit == "percentage" { + spotCheckMinimumPercentageValue := int(math.Round(float64(config.SpotCheckMinimum) / 100.0 * float64(totalCount))) + if spotCheckMinimumPercentageValue == 0 { + return 1 + } + return getSpotChecksMinAsPerMaximum(config.SpotCheckMaximum, spotCheckMinimumPercentageValue) + } + + return getSpotChecksMinAsPerMaximum(config.SpotCheckMaximum, config.SpotCheckMinimum) +} + +func getSpotChecksMinAsPerMaximum(spotCheckMax int, spotCheckMin int) int { + if spotCheckMax == 0 { + return spotCheckMin + } + + if spotCheckMin > spotCheckMax { + return spotCheckMax + } + + return spotCheckMin +} + func analyseSuspiciousExploitable(config fortifyExecuteScanOptions, sys fortify.System, projectVersion *models.ProjectVersion, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string) (int, []*models.ProjectVersionIssueGroup) { log.Entry().Info("Analyzing suspicious and exploitable issues") reducedFilterSelectorSet := sys.ReduceIssueFilterSelectorSet(issueFilterSelectorSet, []string{"Analysis"}, []string{}) diff --git a/cmd/fortifyExecuteScan_generated.go b/cmd/fortifyExecuteScan_generated.go index a605d5582..183c47bd6 100644 --- a/cmd/fortifyExecuteScan_generated.go +++ b/cmd/fortifyExecuteScan_generated.go @@ -65,6 +65,8 @@ type fortifyExecuteScanOptions struct { PullRequestMessageRegexGroup int `json:"pullRequestMessageRegexGroup,omitempty"` DeltaMinutes int `json:"deltaMinutes,omitempty"` SpotCheckMinimum int `json:"spotCheckMinimum,omitempty"` + SpotCheckMinimumUnit string `json:"spotCheckMinimumUnit,omitempty" validate:"possible-values=number percentage"` + SpotCheckMaximum int `json:"SpotCheckMaximum,omitempty"` FprDownloadEndpoint string `json:"fprDownloadEndpoint,omitempty"` VersioningModel string `json:"versioningModel,omitempty" validate:"possible-values=major major-minor semantic full"` PythonInstallCommand string `json:"pythonInstallCommand,omitempty"` @@ -343,7 +345,9 @@ func addFortifyExecuteScanFlags(cmd *cobra.Command, stepConfig *fortifyExecuteSc cmd.Flags().StringVar(&stepConfig.ServerURL, "serverUrl", os.Getenv("PIPER_serverUrl"), "Fortify SSC Url to be used for accessing the APIs") cmd.Flags().IntVar(&stepConfig.PullRequestMessageRegexGroup, "pullRequestMessageRegexGroup", 1, "The group number for extracting the pull request id in `'pullRequestMessageRegex'`") cmd.Flags().IntVar(&stepConfig.DeltaMinutes, "deltaMinutes", 5, "The number of minutes for which an uploaded FPR artifact is considered to be recent and healthy, if exceeded an error will be thrown") - cmd.Flags().IntVar(&stepConfig.SpotCheckMinimum, "spotCheckMinimum", 1, "The minimum number of issues that must be audited per category in the `Spot Checks of each Category` folder to avoid an error being thrown") + cmd.Flags().IntVar(&stepConfig.SpotCheckMinimum, "spotCheckMinimum", 1, "The minimum number/percentage of issues that must be audited per category in the `Spot Checks of each Category` folder to avoid an error being thrown") + cmd.Flags().StringVar(&stepConfig.SpotCheckMinimumUnit, "spotCheckMinimumUnit", `number`, "The unit for the spotCheckMinimum to apply.") + cmd.Flags().IntVar(&stepConfig.SpotCheckMaximum, "SpotCheckMaximum", 0, "The maximum number of issues that must be audited per category in the `Spot Checks of each Category` folder to avoid an error being thrown. Note that this flag depends on the result of spotCheckMinimum. For example if spotCheckMinimum percentage value exceeds spotCheckMaximum then spotCheckMaximum will be considerd else spotCheckMinimum is considered. If zero, this flag will be ignored.") cmd.Flags().StringVar(&stepConfig.FprDownloadEndpoint, "fprDownloadEndpoint", `/download/currentStateFprDownload.html`, "Fortify SSC endpoint for FPR downloads") cmd.Flags().StringVar(&stepConfig.VersioningModel, "versioningModel", `major`, "The default project versioning model used for creating the version based on the build descriptor version to report results in SSC, can be one of `'major'`, `'major-minor'`, `'semantic'`, `'full'`") cmd.Flags().StringVar(&stepConfig.PythonInstallCommand, "pythonInstallCommand", `{{.Pip}} install --user .`, "Additional install command that can be run when `buildTool: 'pip'` is used which allows further customizing the execution environment of the scan") @@ -842,6 +846,24 @@ func fortifyExecuteScanMetadata() config.StepData { Aliases: []config.Alias{}, Default: 1, }, + { + Name: "spotCheckMinimumUnit", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `number`, + }, + { + Name: "SpotCheckMaximum", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "int", + Mandatory: false, + Aliases: []config.Alias{}, + Default: 0, + }, { Name: "fprDownloadEndpoint", ResourceRef: []config.ResourceReference{}, diff --git a/cmd/fortifyExecuteScan_test.go b/cmd/fortifyExecuteScan_test.go index 43052b327..f08d5ca50 100644 --- a/cmd/fortifyExecuteScan_test.go +++ b/cmd/fortifyExecuteScan_test.go @@ -494,7 +494,7 @@ func TestAnalyseSuspiciousExploitable(t *testing.T) { } func TestAnalyseUnauditedIssues(t *testing.T) { - config := fortifyExecuteScanOptions{SpotCheckMinimum: 4, MustAuditIssueGroups: "Audit All, Corporate Security Requirements", SpotAuditIssueGroups: "Spot Checks of Each Category"} + config := fortifyExecuteScanOptions{SpotCheckMinimumUnit: "number", SpotCheckMinimum: 4, MustAuditIssueGroups: "Audit All, Corporate Security Requirements", SpotAuditIssueGroups: "Spot Checks of Each Category"} ff := fortifyMock{} influx := fortifyExecuteScanInflux{} name := "test" @@ -551,6 +551,16 @@ func TestAnalyseUnauditedIssues(t *testing.T) { assert.Equal(t, 3, len(spotChecksCountByCategory)) } +func TestAnalyseUnauditedIssuesWithWrongConfig(t *testing.T) { + config := fortifyExecuteScanOptions{SpotCheckMinimumUnit: "float"} + spotChecksCountByCategory := []fortify.SpotChecksAuditCount{} + ff := fortifyMock{} + auditStatus := map[string]string{} + _, _, err := analyseUnauditedIssues(config, &ff, &models.ProjectVersion{}, &models.FilterSet{}, &models.IssueFilterSelectorSet{}, &fortifyExecuteScanInflux{}, auditStatus, &spotChecksCountByCategory) + assert.Error(t, err) + assert.Equal(t, "Invalid spotCheckMinimumUnit. Please set it as 'percentage' or 'number'.", err.Error()) +} + func TestTriggerFortifyScan(t *testing.T) { t.Run("maven", func(t *testing.T) { dir := t.TempDir() @@ -637,6 +647,30 @@ func TestTriggerFortifyScan(t *testing.T) { }) } +func TestGetMinSpotChecksPerCategory(t *testing.T) { + testExpectedGetMinSpotChecksPerCategory := func(spotChecksMinUnit string, spotChecksMax int, spotChecksMin int, issuesPerCategory int, spotChecksMinCalculatedExpected int) { + testName := fmt.Sprintf("Test GetMinSpotChecksPerCategory for SpotCheckMinimumUnit: %v, SpotCheckMaximum: %v, SpotCheckMinimum: %v, issuesPerCategory: %v", spotChecksMinUnit, spotChecksMax, spotChecksMin, issuesPerCategory) + t.Run(testName, func(t *testing.T) { + config := fortifyExecuteScanOptions{SpotCheckMinimumUnit: spotChecksMinUnit, SpotCheckMaximum: spotChecksMax, SpotCheckMinimum: spotChecksMin} + spotCheckMin := getMinSpotChecksPerCategory(config, issuesPerCategory) + assert.Equal(t, spotChecksMinCalculatedExpected, spotCheckMin) + }) + } + + testExpectedGetMinSpotChecksPerCategory("percentage", 0, 1, 10, 1) + testExpectedGetMinSpotChecksPerCategory("percentage", 10, 10, 3, 1) + testExpectedGetMinSpotChecksPerCategory("percentage", 10, 10, 8, 1) + testExpectedGetMinSpotChecksPerCategory("percentage", 10, 10, 10, 1) + testExpectedGetMinSpotChecksPerCategory("percentage", 10, 10, 24, 2) + testExpectedGetMinSpotChecksPerCategory("percentage", 10, 10, 26, 3) + testExpectedGetMinSpotChecksPerCategory("percentage", 10, 10, 100, 10) + testExpectedGetMinSpotChecksPerCategory("percentage", 10, 10, 200, 10) + testExpectedGetMinSpotChecksPerCategory("percentage", 10, 50, 10, 5) + + testExpectedGetMinSpotChecksPerCategory("number", 0, 1, 10, 1) + testExpectedGetMinSpotChecksPerCategory("number", 5, 10, 100, 5) +} + func TestGenerateAndDownloadQGateReport(t *testing.T) { ffMock := fortifyMock{Successive: false} config := fortifyExecuteScanOptions{ReportTemplateID: 18, ReportType: "PDF"} diff --git a/resources/metadata/fortifyExecuteScan.yaml b/resources/metadata/fortifyExecuteScan.yaml index 5b4ebddd4..f686c9a98 100644 --- a/resources/metadata/fortifyExecuteScan.yaml +++ b/resources/metadata/fortifyExecuteScan.yaml @@ -490,13 +490,39 @@ spec: - name: spotCheckMinimum type: int description: - "The minimum number of issues that must be audited per category in the `Spot Checks of each + "The minimum number/percentage of issues that must be audited per category in the `Spot Checks of each Category` folder to avoid an error being thrown" scope: - PARAMETERS - STAGES - STEPS default: 1 + - name: spotCheckMinimumUnit + type: string + description: + "The unit for the spotCheckMinimum to apply." + scope: + - PARAMETERS + - STAGES + - STEPS + default: 'number' + possibleValues: + - number + - percentage + - name: SpotCheckMaximum + type: int + description: + "The maximum number of issues that must be audited per category in the `Spot Checks of each + Category` folder to avoid an error being thrown. + Note that this flag depends on the result of spotCheckMinimum. + For example if spotCheckMinimum percentage value exceeds spotCheckMaximum then + spotCheckMaximum will be considerd else spotCheckMinimum is considered. + If zero, this flag will be ignored." + scope: + - PARAMETERS + - STAGES + - STEPS + default: 0 - name: fprDownloadEndpoint aliases: - name: fortifyFprDownloadEndpoint