diff --git a/cmd/fortifyExecuteScan.go b/cmd/fortifyExecuteScan.go index f3ca958e2..bb59c2f24 100644 --- a/cmd/fortifyExecuteScan.go +++ b/cmd/fortifyExecuteScan.go @@ -275,7 +275,9 @@ func verifyFFProjectCompliance(config fortifyExecuteScanOptions, sys fortify.Sys return errors.Wrapf(err, "failed to fetch project version issue filter selector for project version ID %v", projectVersion.ID), reports } log.Entry().Debugf("initial filter selector set: %v", issueFilterSelectorSet) - numberOfViolations, issueGroups, err := analyseUnauditedIssues(config, sys, projectVersion, filterSet, issueFilterSelectorSet, influx, auditStatus) + + spotChecksCountByCategory := []fortify.SpotChecksAuditCount{} + numberOfViolations, issueGroups, err := analyseUnauditedIssues(config, sys, projectVersion, filterSet, issueFilterSelectorSet, influx, auditStatus, &spotChecksCountByCategory) if err != nil { return errors.Wrap(err, "failed to analyze unaudited issues"), reports } @@ -290,9 +292,21 @@ func verifyFFProjectCompliance(config fortifyExecuteScanOptions, sys fortify.Sys influx.fortify_data.fields.projectVersionID = projectVersion.ID influx.fortify_data.fields.violations = numberOfViolations - scanReport := fortify.CreateCustomReport(prepareReportData(influx), issueGroups) + fortifyReportingData := prepareReportData(influx) + scanReport := fortify.CreateCustomReport(fortifyReportingData, issueGroups) paths, err := fortify.WriteCustomReports(scanReport, influx.fortify_data.fields.projectName, influx.fortify_data.fields.projectVersion) + if err != nil { + return errors.Wrap(err, "failed to write custom reports"), reports + } reports = append(reports, paths...) + + jsonReport := fortify.CreateJSONReport(fortifyReportingData, spotChecksCountByCategory, config.ServerURL) + paths, err = fortify.WriteJSONReport(jsonReport) + if err != nil { + return errors.Wrap(err, "failed to write json report"), reports + } + reports = append(reports, paths...) + if numberOfViolations > 0 { log.SetErrorCategory(log.ErrorCompliance) return errors.New("fortify scan failed, the project is not compliant. For details check the archived report"), reports @@ -315,10 +329,11 @@ func prepareReportData(influx *fortifyExecuteScanInflux) fortify.FortifyReportDa output.Exploitable = input.exploitable output.Suppressed = input.suppressed output.Suspicious = input.suspicious + output.ProjectVersionID = input.projectVersionID return output } -func analyseUnauditedIssues(config fortifyExecuteScanOptions, sys fortify.System, projectVersion *models.ProjectVersion, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string) (int, []*models.ProjectVersionIssueGroup, error) { +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") reducedFilterSelectorSet := sys.ReduceIssueFilterSelectorSet(issueFilterSelectorSet, []string{"Folder"}, nil) fetchedIssueGroups, err := sys.GetProjectIssuesByIDAndFilterSetGroupedBySelector(projectVersion.ID, "", filterSet.GUID, reducedFilterSelectorSet) @@ -327,7 +342,7 @@ func analyseUnauditedIssues(config fortifyExecuteScanOptions, sys fortify.System } overallViolations := 0 for _, issueGroup := range fetchedIssueGroups { - issueDelta, err := getIssueDeltaFor(config, sys, issueGroup, projectVersion.ID, filterSet, issueFilterSelectorSet, influx, auditStatus) + issueDelta, err := getIssueDeltaFor(config, sys, issueGroup, projectVersion.ID, filterSet, issueFilterSelectorSet, influx, auditStatus, spotChecksCountByCategory) if err != nil { return overallViolations, fetchedIssueGroups, errors.Wrap(err, "failed to get issue delta") } @@ -336,7 +351,7 @@ func analyseUnauditedIssues(config fortifyExecuteScanOptions, sys fortify.System return overallViolations, fetchedIssueGroups, nil } -func getIssueDeltaFor(config fortifyExecuteScanOptions, sys fortify.System, issueGroup *models.ProjectVersionIssueGroup, projectVersionID int64, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string) (int, error) { +func getIssueDeltaFor(config fortifyExecuteScanOptions, sys fortify.System, issueGroup *models.ProjectVersionIssueGroup, projectVersionID int64, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string, spotChecksCountByCategory *[]fortify.SpotChecksAuditCount) (int, error) { totalMinusAuditedDelta := 0 group := "" total := 0 @@ -378,13 +393,13 @@ func getIssueDeltaFor(config fortifyExecuteScanOptions, sys fortify.System, issu if err != nil { return totalMinusAuditedDelta, errors.Wrapf(err, "failed to fetch project version issue groups with filter %v, filter set %v and selector %v for project version ID %v", filter, filterSet, issueFilterSelectorSet, projectVersionID) } - totalMinusAuditedDelta += getSpotIssueCount(config, sys, fetchedIssueGroups, projectVersionID, filterSet, reducedFilterSelectorSet, influx, auditStatus) + totalMinusAuditedDelta += getSpotIssueCount(config, sys, fetchedIssueGroups, projectVersionID, filterSet, reducedFilterSelectorSet, influx, auditStatus, spotChecksCountByCategory) } } return totalMinusAuditedDelta, nil } -func getSpotIssueCount(config fortifyExecuteScanOptions, sys fortify.System, spotCheckCategories []*models.ProjectVersionIssueGroup, projectVersionID int64, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string) int { +func getSpotIssueCount(config fortifyExecuteScanOptions, sys fortify.System, spotCheckCategories []*models.ProjectVersionIssueGroup, projectVersionID int64, filterSet *models.FilterSet, issueFilterSelectorSet *models.IssueFilterSelectorSet, influx *fortifyExecuteScanInflux, auditStatus map[string]string, spotChecksCountByCategory *[]fortify.SpotChecksAuditCount) int { overallDelta := 0 overallIssues := 0 overallIssuesAudited := 0 @@ -418,6 +433,7 @@ func getSpotIssueCount(config fortifyExecuteScanOptions, sys fortify.System, spo overallIssuesAudited += audited auditStatus[group] = fmt.Sprintf("%v total : %v audited %v", total, audited, flagOutput) + *spotChecksCountByCategory = append(*spotChecksCountByCategory, fortify.SpotChecksAuditCount{Audited: audited, Total: total, Type: group}) } influx.fortify_data.fields.spotChecksTotal = overallIssues diff --git a/cmd/fortifyExecuteScan_test.go b/cmd/fortifyExecuteScan_test.go index 747b9ec6b..0c953f4d3 100644 --- a/cmd/fortifyExecuteScan_test.go +++ b/cmd/fortifyExecuteScan_test.go @@ -491,7 +491,9 @@ func TestAnalyseUnauditedIssues(t *testing.T) { }, }, } - issues, groups, err := analyseUnauditedIssues(config, &ff, &projectVersion, &models.FilterSet{}, &selectorSet, &influx, auditStatus) + + spotChecksCountByCategory := []fortify.SpotChecksAuditCount{} + issues, groups, err := analyseUnauditedIssues(config, &ff, &projectVersion, &models.FilterSet{}, &selectorSet, &influx, auditStatus, &spotChecksCountByCategory) assert.NoError(t, err) assert.Equal(t, 13, issues) assert.Equal(t, 3, len(groups)) @@ -503,6 +505,7 @@ func TestAnalyseUnauditedIssues(t *testing.T) { assert.Equal(t, 13, influx.fortify_data.fields.spotChecksTotal) assert.Equal(t, 11, influx.fortify_data.fields.spotChecksAudited) assert.Equal(t, 1, influx.fortify_data.fields.spotChecksGap) + assert.Equal(t, 3, len(spotChecksCountByCategory)) } func TestTriggerFortifyScan(t *testing.T) { diff --git a/pkg/fortify/fortify_test.go b/pkg/fortify/fortify_test.go index 3388dd1d0..853c2668d 100644 --- a/pkg/fortify/fortify_test.go +++ b/pkg/fortify/fortify_test.go @@ -1314,3 +1314,36 @@ func TestBase64EndodePlainToken(t *testing.T) { assert.Equal(t, "OTUzODcwNDYtNWFjOC00NTcwLTg3NWQtYTVlYzhiZDhkM2Qy", encodedToken) }) } + +func TestCreateJSONReport(t *testing.T) { + t.Run("test success", func(t *testing.T) { + spotChecksCountByCategory := []SpotChecksAuditCount{} + spotChecksCountByCategory = append(spotChecksCountByCategory, SpotChecksAuditCount{Audited: 3, Total: 3, Type: "J2EE Misconfiguration: Missing Error Handling"}) + spotChecksCountByCategory = append(spotChecksCountByCategory, SpotChecksAuditCount{Audited: 1, Total: 3, Type: "J2EE Bad Practices: Leftover Debug Code"}) + fortifyReportData := FortifyReportData{CorporateAudited: 30, CorporateTotal: 30, AuditAllTotal: 1, AuditAllAudited: 1, ProjectVersionID: 4999} + jsonReport := CreateJSONReport(fortifyReportData, spotChecksCountByCategory, "https://fortify-test.com/ssc") + assert.Equal(t, true, jsonReport.AtleastOneSpotChecksCategoryAudited) + assert.Equal(t, 1, jsonReport.AuditAllAudited) + assert.Equal(t, 1, jsonReport.AuditAllTotal) + assert.Equal(t, 30, jsonReport.CorporateAudited) + assert.Equal(t, 30, jsonReport.CorporateTotal) + assert.Equal(t, "https://fortify-test.com/ssc/html/ssc/version/4999", jsonReport.URL) + assert.Equal(t, "https://fortify-test.com/ssc", jsonReport.ToolInstance) + }) + + t.Run("atleast one category spotchecks failed", func(t *testing.T) { + spotChecksCountByCategory := []SpotChecksAuditCount{} + spotChecksCountByCategory = append(spotChecksCountByCategory, SpotChecksAuditCount{Audited: 3, Total: 3, Type: "J2EE Misconfiguration: Missing Error Handling"}) + spotChecksCountByCategory = append(spotChecksCountByCategory, SpotChecksAuditCount{Audited: 0, Total: 1, Type: "J2EE Bad Practices: Leftover Debug Code"}) + fortifyReportData := FortifyReportData{CorporateAudited: 0, CorporateTotal: 0, AuditAllTotal: 0, AuditAllAudited: 0} + jsonReport := CreateJSONReport(fortifyReportData, spotChecksCountByCategory, "https://fortify-test.com/ssc") + assert.Equal(t, false, jsonReport.AtleastOneSpotChecksCategoryAudited) + }) + + t.Run("no spot checks audited", func(t *testing.T) { + spotChecksCountByCategory := []SpotChecksAuditCount{} + fortifyReportData := FortifyReportData{CorporateAudited: 0, CorporateTotal: 0, AuditAllTotal: 0, AuditAllAudited: 0} + jsonReport := CreateJSONReport(fortifyReportData, spotChecksCountByCategory, "https://fortify-test.com/ssc") + assert.Equal(t, true, jsonReport.AtleastOneSpotChecksCategoryAudited) + }) +} diff --git a/pkg/fortify/reporting.go b/pkg/fortify/reporting.go index 559ef9d1d..bc0023939 100644 --- a/pkg/fortify/reporting.go +++ b/pkg/fortify/reporting.go @@ -2,8 +2,10 @@ package fortify import ( "crypto/sha1" + "encoding/json" "fmt" "path/filepath" + "strconv" "strings" "time" @@ -16,19 +18,31 @@ import ( ) type FortifyReportData struct { - ProjectName string - ProjectVersion string - Violations int - CorporateTotal int - CorporateAudited int - AuditAllTotal int - AuditAllAudited int - SpotChecksTotal int - SpotChecksAudited int - SpotChecksGap int - Suspicious int - Exploitable int - Suppressed int + ToolName string `json:"toolName"` + ToolInstance string `json:"toolInstance"` + ProjectName string `json:"projectName"` + ProjectVersion string `json:"projectVersion"` + ProjectVersionID int64 `json:"projectVersionID"` + Violations int `json:"violations"` + CorporateTotal int `json:"corporateTotal"` + CorporateAudited int `json:"corporateAudited"` + AuditAllTotal int `json:"auditAllTotal"` + AuditAllAudited int `json:"auditAllAudited"` + SpotChecksTotal int `json:"spotChecksTotal"` + SpotChecksAudited int `json:"spotChecksAudited"` + SpotChecksGap int `json:"spotChecksGap"` + Suspicious int `json:"suspicious"` + Exploitable int `json:"exploitable"` + Suppressed int `json:"suppressed"` + AtleastOneSpotChecksCategoryAudited bool `json:"atleastOneSpotChecksCategoryAudited"` + URL string `json:"url"` + SpotChecksCategories *[]SpotChecksAuditCount `json:"spotChecksCategories"` +} + +type SpotChecksAuditCount struct { + Audited int `json:"spotChecksCategories"` + Total int `json:"total"` + Type string `json:"type"` } func CreateCustomReport(data FortifyReportData, issueGroups []*models.ProjectVersionIssueGroup) reporting.ScanReport { @@ -77,7 +91,45 @@ func CreateCustomReport(data FortifyReportData, issueGroups []*models.ProjectVer return scanReport } -func WriteCustomReports(scanReport reporting.ScanReport, projectName, projectVersion string) ([]piperutils.Path, error) { +func CreateJSONReport(reportData FortifyReportData, spotChecksCountByCategory []SpotChecksAuditCount, serverURL string) FortifyReportData { + reportData.AtleastOneSpotChecksCategoryAudited = true + for _, spotChecksElement := range spotChecksCountByCategory { + if spotChecksElement.Total > 0 && spotChecksElement.Audited == 0 { + reportData.AtleastOneSpotChecksCategoryAudited = false + break + } + } + + reportData.SpotChecksCategories = &spotChecksCountByCategory + reportData.URL = serverURL + "/html/ssc/version/" + strconv.FormatInt(reportData.ProjectVersionID, 10) + reportData.ToolInstance = serverURL + reportData.ToolName = "fortify" + + return reportData +} + +func WriteJSONReport(jsonReport FortifyReportData) ([]piperutils.Path, error) { + utils := piperutils.Files{} + reportPaths := []piperutils.Path{} + + // Standard JSON Report + jsonComplianceReportPath := filepath.Join(ReportsDirectory, "piper_fortify_report.json") + // Ensure reporting directory exists + if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil { + return reportPaths, errors.Wrapf(err, "failed to create report directory") + } + + file, _ := json.Marshal(jsonReport) + if err := utils.FileWrite(jsonComplianceReportPath, file, 0666); err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return reportPaths, errors.Wrapf(err, "failed to write fortify json compliance report") + } + reportPaths = append(reportPaths, piperutils.Path{Name: "Fortify JSON Compliance Report", Target: jsonComplianceReportPath}) + + return reportPaths, nil +} + +func WriteCustomReports(scanReport reporting.ScanReport, projectName string, projectVersion string) ([]piperutils.Path, error) { utils := piperutils.Files{} reportPaths := []piperutils.Path{} @@ -109,7 +161,6 @@ func WriteCustomReports(scanReport reporting.ScanReport, projectName, projectVer // we do not add the json report to the overall list of reports for now, // since it is just an intermediary report used as input for later // and there does not seem to be real benefit in archiving it. - return reportPaths, nil }