Skip to content

Commit 6771e25

Browse files
authored
Add option to fail due to FailBuild flag on violations for repo scan (#1047)
1 parent 58b5103 commit 6771e25

File tree

4 files changed

+68
-42
lines changed

4 files changed

+68
-42
lines changed

scanrepository/scanrepository.go

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/jfrog/froggit-go/vcsclient"
1616
"github.com/jfrog/froggit-go/vcsutils"
1717
"github.com/jfrog/gofrog/version"
18+
"github.com/jfrog/jfrog-cli-security/policy"
1819
"github.com/jfrog/jfrog-cli-security/utils/formats"
1920
"github.com/jfrog/jfrog-cli-security/utils/jasutils"
2021
"github.com/jfrog/jfrog-cli-security/utils/results"
@@ -73,18 +74,25 @@ func (cfp *ScanRepositoryCmd) scanAndFixRepository(repository *utils.Repository,
7374
return
7475
}
7576
log.Debug(fmt.Sprintf("Detected branches for scan: %s", strings.Join(repository.Branches, ", ")))
77+
anyHasFailBuild := false
7678
for _, branch := range repository.Branches {
7779
log.Debug(fmt.Sprintf("Scanning '%s' branch...", branch))
7880
cfp.scanDetails.SetBaseBranch(branch)
7981
cfp.scanDetails.SetXscGitInfoContext(branch, repository.Project, client)
80-
if err = cfp.scanAndFixBranch(repository); err != nil {
81-
return
82+
hadFailBuild, e := cfp.scanAndFixBranch(repository)
83+
if e != nil {
84+
return e
8285
}
86+
anyHasFailBuild = anyHasFailBuild || hadFailBuild
87+
}
88+
// Check if we need to output error due to fail the build flag on violations if repository configuration requires it
89+
if repository.FailBuildOnViolations != nil && *repository.FailBuildOnViolations && anyHasFailBuild {
90+
return policy.NewFailBuildError()
8391
}
8492
return
8593
}
8694

87-
func (cfp *ScanRepositoryCmd) scanAndFixBranch(repository *utils.Repository) (err error) {
95+
func (cfp *ScanRepositoryCmd) scanAndFixBranch(repository *utils.Repository) (hasFailBuild bool, err error) {
8896
repoDir, restoreBaseDir, err := cfp.cloneRepositoryOrUseLocalAndCheckoutToBranch()
8997
if err != nil {
9098
return
@@ -115,10 +123,11 @@ func (cfp *ScanRepositoryCmd) scanAndFixBranch(repository *utils.Repository) (er
115123
for i := range repository.Projects {
116124
cfp.scanDetails.Project = &repository.Projects[i]
117125
cfp.projectTech = []techutils.Technology{}
118-
if findings, e := cfp.scanAndFixProject(repository); e != nil {
119-
return e
126+
if hasFailBuildInProject, findings, e := cfp.scanAndFixProject(repository); e != nil {
127+
return false, e
120128
} else {
121129
totalFindings += findings
130+
hasFailBuild = hasFailBuild || hasFailBuildInProject
122131
}
123132
}
124133

@@ -161,8 +170,9 @@ func (cfp *ScanRepositoryCmd) setCommandPrerequisites(repository *utils.Reposito
161170
return
162171
}
163172

164-
func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) (int, error) {
173+
func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) (bool, int, error) {
165174
var isFixNeeded bool
175+
shouldFailBuild := false
166176
totalFindings := 0
167177
// A map that contains the full project paths as a keys
168178
// The value is a map of vulnerable package names -> the scanDetails of the vulnerable packages.
@@ -173,20 +183,12 @@ func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) (i
173183
scanResults, err := cfp.scan(fullPathWd)
174184
if err != nil {
175185
if err = utils.CreateErrorIfPartialResultsDisabled(cfp.scanDetails.AllowPartialResults(), fmt.Sprintf("An error occurred during Audit execution for '%s' working directory. Fixes will be skipped for this working directory", fullPathWd), err); err != nil {
176-
return totalFindings, err
186+
return shouldFailBuild, totalFindings, err
177187
}
178188
continue
179189
}
180-
if summary, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: scanResults.IncludesVulnerabilities(), HasViolationContext: scanResults.HasViolationContext()}).ConvertToSummary(scanResults); err != nil {
181-
return totalFindings, err
182-
} else {
183-
findingCount := summary.GetTotalViolations()
184-
if findingCount == 0 {
185-
findingCount = summary.GetTotalVulnerabilities()
186-
}
187-
totalFindings += findingCount
188-
}
189-
190+
totalFindings += getTotalFindingsFromScanResults(scanResults)
191+
shouldFailBuild = shouldFailBuild || (scanResults.Violations != nil && scanResults.Violations.ShouldFailBuild())
190192
if repository.GitProvider.String() == vcsutils.GitHub.String() {
191193
// Uploads Sarif results to GitHub in order to view the scan in the code scanning UI
192194
// Currently available on GitHub only
@@ -203,42 +205,56 @@ func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) (i
203205
if repository.DetectionOnly {
204206
continue
205207
}
206-
207208
for _, target := range scanResults.Targets {
208-
targetPath := target.Target
209-
convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{
210-
IncludeVulnerabilities: scanResults.IncludesVulnerabilities(),
211-
HasViolationContext: scanResults.HasViolationContext(),
212-
IncludeTargets: []string{targetPath},
213-
})
214-
simpleJsonResult, err := convertor.ConvertToSimpleJson(scanResults)
215-
if err != nil {
216-
if err = utils.CreateErrorIfPartialResultsDisabled(cfp.scanDetails.AllowPartialResults(), fmt.Sprintf("An error occurred while preparing the vulnerabilities map for '%s' working directory. Fixes will be skipped for this working directory", targetPath), err); err != nil {
217-
return totalFindings, err
218-
}
219-
continue
220-
}
221-
222-
currPathVulnerabilities, err := cfp.createVulnerabilitiesMapFromSimpleJson(simpleJsonResult)
209+
currPathVulnerabilities, err := cfp.populateTargetResultsInVulnMap(target, scanResults)
223210
if err != nil {
224-
if err = utils.CreateErrorIfPartialResultsDisabled(cfp.scanDetails.AllowPartialResults(), fmt.Sprintf("An error occurred while preparing the vulnerabilities map for '%s' working directory. Fixes will be skipped for this working directory", targetPath), err); err != nil {
225-
return totalFindings, err
211+
if err = utils.CreateErrorIfPartialResultsDisabled(cfp.scanDetails.AllowPartialResults(), fmt.Sprintf("An error occurred while preparing the vulnerabilities map for '%s' working directory. Fixes will be skipped for this working directory", target.Target), err); err != nil {
212+
return shouldFailBuild, totalFindings, err
226213
}
227214
continue
228215
}
229216
if len(currPathVulnerabilities) > 0 {
230217
isFixNeeded = true
231-
log.Debug(fmt.Sprintf("Found %d fixable vulnerabilities in '%s': %s", len(currPathVulnerabilities), targetPath, strings.Join(maps.Keys(currPathVulnerabilities), ", ")))
218+
log.Debug(fmt.Sprintf("Found %d fixable vulnerabilities in '%s': %s", len(currPathVulnerabilities), target.Target, strings.Join(maps.Keys(currPathVulnerabilities), ", ")))
232219
}
233-
vulnerabilitiesByPathMap[targetPath] = currPathVulnerabilities
220+
vulnerabilitiesByPathMap[target.Target] = currPathVulnerabilities
234221
}
235222
}
236223
if repository.DetectionOnly {
237224
log.Info(fmt.Sprintf("This command is running in detection mode only. To enable automatic fixing of issues, set the '%s' environment variable to 'false'.", utils.DetectionOnlyEnv))
238225
} else if isFixNeeded {
239-
return totalFindings, cfp.fixVulnerablePackages(repository, vulnerabilitiesByPathMap)
226+
return shouldFailBuild, totalFindings, cfp.fixVulnerablePackages(repository, vulnerabilitiesByPathMap)
227+
}
228+
return shouldFailBuild, totalFindings, nil
229+
}
230+
231+
func getTotalFindingsFromScanResults(scanResults *results.SecurityCommandResults) int {
232+
summary, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: scanResults.IncludesVulnerabilities(), HasViolationContext: scanResults.HasViolationContext()}).ConvertToSummary(scanResults)
233+
if err != nil {
234+
return 0
235+
}
236+
findingCount := summary.GetTotalViolations()
237+
if findingCount == 0 {
238+
findingCount = summary.GetTotalVulnerabilities()
239+
}
240+
return findingCount
241+
}
242+
243+
func (cfp *ScanRepositoryCmd) populateTargetResultsInVulnMap(target *results.TargetResults, scanResults *results.SecurityCommandResults) (map[string]*utils.VulnerabilityDetails, error) {
244+
convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{
245+
IncludeVulnerabilities: scanResults.IncludesVulnerabilities(),
246+
HasViolationContext: scanResults.HasViolationContext(),
247+
IncludeTargets: []string{target.Target},
248+
})
249+
simpleJsonResult, err := convertor.ConvertToSimpleJson(scanResults)
250+
if err != nil {
251+
return nil, fmt.Errorf("an error occurred while converting scan results to simple json for target '%s': %w", target.Target, err)
252+
}
253+
currPathVulnerabilities, err := cfp.createVulnerabilitiesMapFromSimpleJson(simpleJsonResult)
254+
if err != nil {
255+
return nil, fmt.Errorf("an error occurred while creating vulnerabilities map from simple json for target '%s': %w", target.Target, err)
240256
}
241-
return totalFindings, nil
257+
return currPathVulnerabilities, nil
242258
}
243259

244260
// Audit the dependencies of the current commit.

scanrepository/scanrepository_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ func TestCreateVulnerabilitiesMap(t *testing.T) {
699699

700700
func TestMultiTargetVulnerabilitiesMap(t *testing.T) {
701701
cfp := &ScanRepositoryCmd{}
702-
702+
703703
scanResults := &results.SecurityCommandResults{
704704
ResultsMetaData: results.ResultsMetaData{ResultContext: results.ResultContext{IncludeVulnerabilities: true}},
705705
Targets: []*results.TargetResults{
@@ -739,7 +739,7 @@ func TestMultiTargetVulnerabilitiesMap(t *testing.T) {
739739
},
740740
},
741741
}
742-
742+
743743
convertor1 := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{
744744
IncludeVulnerabilities: true,
745745
IncludeTargets: []string{"project1"},
@@ -751,7 +751,7 @@ func TestMultiTargetVulnerabilitiesMap(t *testing.T) {
751751
assert.Len(t, vulnsMap1, 1)
752752
assert.Contains(t, vulnsMap1, "pkg1")
753753
assert.NotContains(t, vulnsMap1, "pkg2")
754-
754+
755755
convertor2 := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{
756756
IncludeVulnerabilities: true,
757757
IncludeTargets: []string{"project2"},

utils/consts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const (
7171
AvoidPreviousPrCommentsDeletionEnv = "JF_AVOID_PREVIOUS_PR_COMMENTS_DELETION"
7272
AddPrCommentOnSuccessEnv = "JF_PR_ADD_SUCCESS_COMMENT"
7373
FailOnSecurityIssuesEnv = "JF_FAIL"
74+
FailBuildOnViolationsEnv = "JF_REPO_SCAN_FAIL_BUILD"
7475
UseWrapperEnv = "JF_USE_WRAPPER"
7576
DepsRepoEnv = "JF_DEPS_REPO"
7677
MinSeverityEnv = "JF_MIN_SEVERITY"

utils/params.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ type Scan struct {
165165
FixableOnly bool `yaml:"fixableOnly,omitempty"`
166166
DetectionOnly bool `yaml:"skipAutoFix,omitempty"`
167167
FailOnSecurityIssues *bool `yaml:"failOnSecurityIssues,omitempty"`
168+
FailBuildOnViolations *bool `yaml:"failBuildOnViolations,omitempty"`
168169
AvoidPreviousPrCommentsDeletion bool `yaml:"avoidPreviousPrCommentsDeletion,omitempty"`
169170
MinSeverity string `yaml:"minSeverity,omitempty"`
170171
DisableJas bool `yaml:"disableJas,omitempty"`
@@ -251,6 +252,14 @@ func (s *Scan) setDefaultsIfNeeded() (err error) {
251252
}
252253
s.FailOnSecurityIssues = &failOnSecurityIssues
253254
}
255+
if s.FailBuildOnViolations == nil {
256+
var failBuildOnViolations bool
257+
// Default is false for backward compatibility
258+
if failBuildOnViolations, err = getBoolEnv(FailBuildOnViolationsEnv, false); err != nil {
259+
return
260+
}
261+
s.FailBuildOnViolations = &failBuildOnViolations
262+
}
254263
if s.MinSeverity == "" {
255264
if err = readParamFromEnv(MinSeverityEnv, &s.MinSeverity); err != nil && !e.IsMissingEnvErr(err) {
256265
return

0 commit comments

Comments
 (0)