github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/checkmarx/reporting.go (about) 1 package checkmarx 2 3 import ( 4 "bytes" 5 "crypto/sha1" 6 "encoding/json" 7 "fmt" 8 "math" 9 "path/filepath" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/SAP/jenkins-library/pkg/format" 15 "github.com/SAP/jenkins-library/pkg/log" 16 "github.com/SAP/jenkins-library/pkg/piperutils" 17 "github.com/SAP/jenkins-library/pkg/reporting" 18 19 "github.com/pkg/errors" 20 ) 21 22 type CheckmarxReportData struct { 23 ToolName string `json:"toolName"` 24 ProjectName string `json:"projectName"` 25 ProjectID int64 `json:"projectID"` 26 ScanID int64 `json:"scanID"` 27 TeamName string `json:"teamName"` 28 TeamPath string `json:"teamPath"` 29 DeepLink string `json:"deepLink"` 30 Preset string `json:"preset"` 31 CheckmarxVersion string `json:"checkmarxVersion"` 32 ScanType string `json:"scanType"` 33 HighTotal int `json:"highTotal"` 34 HighAudited int `json:"highAudited"` 35 MediumTotal int `json:"mediumTotal"` 36 MediumAudited int `json:"mediumAudited"` 37 LowTotal int `json:"lowTotal"` 38 LowAudited int `json:"lowAudited"` 39 InformationTotal int `json:"informationTotal"` 40 InformationAudited int `json:"informationAudited"` 41 IsLowPerQueryAudited bool `json:"isLowPerQueryAudited"` 42 LowPerQuery *[]LowPerQuery `json:"lowPerQuery"` 43 } 44 45 type LowPerQuery struct { 46 QueryName string `json:"query"` 47 Audited int `json:"audited"` 48 Total int `json:"total"` 49 } 50 51 func CreateCustomReport(data map[string]interface{}, insecure, neutral []string) reporting.ScanReport { 52 deepLink := fmt.Sprintf(`<a href="%v" target="_blank">Link to scan in CX UI</a>`, data["DeepLink"]) 53 54 scanReport := reporting.ScanReport{ 55 ReportTitle: "Checkmarx SAST Report", 56 Subheaders: []reporting.Subheader{ 57 {Description: "Project name", Details: fmt.Sprint(data["ProjectName"])}, 58 {Description: "Project ID", Details: fmt.Sprint(data["ProjectId"])}, 59 {Description: "Owner", Details: fmt.Sprint(data["Owner"])}, 60 {Description: "Scan ID", Details: fmt.Sprint(data["ScanId"])}, 61 {Description: "Team", Details: fmt.Sprint(data["Team"])}, 62 {Description: "Team full path", Details: fmt.Sprint(data["TeamFullPathOnReportDate"])}, 63 {Description: "Scan start", Details: fmt.Sprint(data["ScanStart"])}, 64 {Description: "Scan duration", Details: fmt.Sprint(data["ScanTime"])}, 65 {Description: "Scan type", Details: fmt.Sprint(data["ScanType"])}, 66 {Description: "Preset", Details: fmt.Sprint(data["Preset"])}, 67 {Description: "Report creation time", Details: fmt.Sprint(data["ReportCreationTime"])}, 68 {Description: "Lines of code scanned", Details: fmt.Sprint(data["LinesOfCodeScanned"])}, 69 {Description: "Files scanned", Details: fmt.Sprint(data["FilesScanned"])}, 70 {Description: "Checkmarx version", Details: fmt.Sprint(data["CheckmarxVersion"])}, 71 {Description: "Deep link", Details: deepLink}, 72 }, 73 Overview: []reporting.OverviewRow{}, 74 ReportTime: time.Now(), 75 } 76 77 for _, issue := range insecure { 78 row := reporting.OverviewRow{} 79 row.Description = fmt.Sprint(issue) 80 row.Style = reporting.Red 81 82 scanReport.Overview = append(scanReport.Overview, row) 83 } 84 for _, issue := range neutral { 85 row := reporting.OverviewRow{} 86 row.Description = fmt.Sprint(issue) 87 88 scanReport.Overview = append(scanReport.Overview, row) 89 } 90 91 detailTable := reporting.ScanDetailTable{ 92 Headers: []string{ 93 "KPI", 94 "Count", 95 }, 96 WithCounter: false, 97 } 98 detailRows := []reporting.OverviewRow{ 99 {Description: "High issues", Details: fmt.Sprint(data["High"].(map[string]int)["Issues"])}, 100 {Description: "High not false positive issues", Details: fmt.Sprint(data["High"].(map[string]int)["NotFalsePositive"])}, 101 {Description: "High not exploitable issues", Details: fmt.Sprint(data["High"].(map[string]int)["NotExploitable"])}, 102 {Description: "High confirmed issues", Details: fmt.Sprint(data["High"].(map[string]int)["Confirmed"])}, 103 {Description: "High urgent issues", Details: fmt.Sprint(data["High"].(map[string]int)["Urgent"])}, 104 {Description: "High proposed not exploitable issues", Details: fmt.Sprint(data["High"].(map[string]int)["ProposedNotExploitable"])}, 105 {Description: "High to verify issues", Details: fmt.Sprint(data["High"].(map[string]int)["ToVerify"])}, 106 {Description: "Medium issues", Details: fmt.Sprint(data["Medium"].(map[string]int)["Issues"])}, 107 {Description: "Medium not false positive issues", Details: fmt.Sprint(data["Medium"].(map[string]int)["NotFalsePositive"])}, 108 {Description: "Medium not exploitable issues", Details: fmt.Sprint(data["Medium"].(map[string]int)["NotExploitable"])}, 109 {Description: "Medium confirmed issues", Details: fmt.Sprint(data["Medium"].(map[string]int)["Confirmed"])}, 110 {Description: "Medium urgent issues", Details: fmt.Sprint(data["Medium"].(map[string]int)["Urgent"])}, 111 {Description: "Medium proposed not exploitable issues", Details: fmt.Sprint(data["Medium"].(map[string]int)["ProposedNotExploitable"])}, 112 {Description: "Medium to verify issues", Details: fmt.Sprint(data["Medium"].(map[string]int)["ToVerify"])}, 113 {Description: "Low issues", Details: fmt.Sprint(data["Low"].(map[string]int)["Issues"])}, 114 {Description: "Low not false positive issues", Details: fmt.Sprint(data["Low"].(map[string]int)["NotFalsePositive"])}, 115 {Description: "Low not exploitable issues", Details: fmt.Sprint(data["Low"].(map[string]int)["NotExploitable"])}, 116 {Description: "Low confirmed issues", Details: fmt.Sprint(data["Low"].(map[string]int)["Confirmed"])}, 117 {Description: "Low urgent issues", Details: fmt.Sprint(data["Low"].(map[string]int)["Urgent"])}, 118 {Description: "Low proposed not exploitable issues", Details: fmt.Sprint(data["Low"].(map[string]int)["ProposedNotExploitable"])}, 119 {Description: "Low to verify issues", Details: fmt.Sprint(data["Low"].(map[string]int)["ToVerify"])}, 120 {Description: "Informational issues", Details: fmt.Sprint(data["Information"].(map[string]int)["Issues"])}, 121 {Description: "Informational not false positive issues", Details: fmt.Sprint(data["Information"].(map[string]int)["NotFalsePositive"])}, 122 {Description: "Informational not exploitable issues", Details: fmt.Sprint(data["Information"].(map[string]int)["NotExploitable"])}, 123 {Description: "Informational confirmed issues", Details: fmt.Sprint(data["Information"].(map[string]int)["Confirmed"])}, 124 {Description: "Informational urgent issues", Details: fmt.Sprint(data["Information"].(map[string]int)["Urgent"])}, 125 {Description: "Informational proposed not exploitable issues", Details: fmt.Sprint(data["Information"].(map[string]int)["ProposedNotExploitable"])}, 126 {Description: "Informational to verify issues", Details: fmt.Sprint(data["Information"].(map[string]int)["ToVerify"])}, 127 } 128 for _, detailRow := range detailRows { 129 row := reporting.ScanRow{} 130 row.AddColumn(detailRow.Description, 0) 131 row.AddColumn(detailRow.Details, 0) 132 133 detailTable.Rows = append(detailTable.Rows, row) 134 } 135 scanReport.DetailTable = detailTable 136 137 return scanReport 138 } 139 140 func CreateJSONReport(data map[string]interface{}) CheckmarxReportData { 141 checkmarxReportData := CheckmarxReportData{ 142 ToolName: `checkmarx`, 143 ProjectName: fmt.Sprint(data["ProjectName"]), 144 TeamName: fmt.Sprint(data["Team"]), 145 TeamPath: fmt.Sprint(data["TeamFullPathOnReportDate"]), 146 DeepLink: fmt.Sprint(data["DeepLink"]), 147 Preset: fmt.Sprint(data["Preset"]), 148 CheckmarxVersion: fmt.Sprint(data["CheckmarxVersion"]), 149 ScanType: fmt.Sprint(data["ScanType"]), 150 } 151 152 if s, err := strconv.ParseInt(fmt.Sprint(data["ProjectId"]), 10, 64); err == nil { 153 checkmarxReportData.ProjectID = s 154 } 155 156 if s, err := strconv.ParseInt(fmt.Sprint(data["ScanId"]), 10, 64); err == nil { 157 checkmarxReportData.ScanID = s 158 } 159 160 checkmarxReportData.HighAudited = data["High"].(map[string]int)["Issues"] - data["High"].(map[string]int)["NotFalsePositive"] 161 checkmarxReportData.HighTotal = data["High"].(map[string]int)["Issues"] 162 163 checkmarxReportData.MediumAudited = data["Medium"].(map[string]int)["Issues"] - data["Medium"].(map[string]int)["NotFalsePositive"] 164 checkmarxReportData.MediumTotal = data["Medium"].(map[string]int)["Issues"] 165 166 checkmarxReportData.LowAudited = data["Low"].(map[string]int)["Confirmed"] + data["Low"].(map[string]int)["NotExploitable"] 167 checkmarxReportData.LowTotal = data["Low"].(map[string]int)["Issues"] 168 169 checkmarxReportData.InformationAudited = data["Information"].(map[string]int)["Confirmed"] + data["Information"].(map[string]int)["NotExploitable"] 170 checkmarxReportData.InformationTotal = data["Information"].(map[string]int)["Issues"] 171 172 lowPerQueryList := []LowPerQuery{} 173 checkmarxReportData.IsLowPerQueryAudited = true 174 if _, ok := data["LowPerQuery"]; ok { 175 lowPerQueryMap := data["LowPerQuery"].(map[string]map[string]int) 176 for queryName, resultsLowQuery := range lowPerQueryMap { 177 audited := resultsLowQuery["Confirmed"] + resultsLowQuery["NotExploitable"] 178 total := resultsLowQuery["Issues"] 179 lowPerQuery := LowPerQuery{} 180 lowPerQuery.QueryName = queryName 181 lowPerQuery.Audited = audited 182 lowPerQuery.Total = total 183 lowAuditedRequiredPerQuery := int(math.Ceil(0.10 * float64(total))) 184 if audited < lowAuditedRequiredPerQuery && audited < 10 { 185 checkmarxReportData.IsLowPerQueryAudited = false 186 } 187 lowPerQueryList = append(lowPerQueryList, lowPerQuery) 188 } 189 } 190 checkmarxReportData.LowPerQuery = &lowPerQueryList 191 192 return checkmarxReportData 193 } 194 195 func WriteJSONReport(jsonReport CheckmarxReportData) ([]piperutils.Path, error) { 196 utils := piperutils.Files{} 197 reportPaths := []piperutils.Path{} 198 199 // Standard JSON Report 200 jsonComplianceReportPath := filepath.Join(ReportsDirectory, "piper_checkmarx_report.json") 201 // Ensure reporting directory exists 202 if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil { 203 return reportPaths, errors.Wrapf(err, "failed to create report directory") 204 } 205 206 file, _ := json.Marshal(jsonReport) 207 if err := utils.FileWrite(jsonComplianceReportPath, file, 0666); err != nil { 208 log.SetErrorCategory(log.ErrorConfiguration) 209 return reportPaths, errors.Wrapf(err, "failed to write Checkmarx JSON compliance report") 210 } 211 reportPaths = append(reportPaths, piperutils.Path{Name: "Checkmarx JSON Compliance Report", Target: jsonComplianceReportPath}) 212 213 return reportPaths, nil 214 } 215 216 // WriteSarif writes a json file to disk as a .sarif if it respects the specification declared in format.SARIF 217 func WriteSarif(sarif format.SARIF) ([]piperutils.Path, error) { 218 utils := piperutils.Files{} 219 reportPaths := []piperutils.Path{} 220 221 sarifReportPath := filepath.Join(ReportsDirectory, "result.sarif") 222 // Ensure reporting directory exists 223 if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil { 224 return reportPaths, errors.Wrapf(err, "failed to create report directory") 225 } 226 227 // HTML characters will most likely be present: we need to use encode: create a buffer to hold JSON data 228 buffer := new(bytes.Buffer) 229 // create JSON encoder for buffer 230 bufEncoder := json.NewEncoder(buffer) 231 // set options 232 bufEncoder.SetEscapeHTML(false) 233 bufEncoder.SetIndent("", " ") 234 //encode to buffer 235 bufEncoder.Encode(sarif) 236 log.Entry().Info("Writing file to disk: ", sarifReportPath) 237 if err := utils.FileWrite(sarifReportPath, buffer.Bytes(), 0666); err != nil { 238 log.SetErrorCategory(log.ErrorConfiguration) 239 return reportPaths, errors.Wrapf(err, "failed to write Checkmarx SARIF report") 240 } 241 reportPaths = append(reportPaths, piperutils.Path{Name: "Checkmarx SARIF Report", Target: sarifReportPath}) 242 243 return reportPaths, nil 244 } 245 246 func WriteCustomReports(scanReport reporting.ScanReport, projectName, projectID string) ([]piperutils.Path, error) { 247 utils := piperutils.Files{} 248 reportPaths := []piperutils.Path{} 249 250 // ignore templating errors since template is in our hands and issues will be detected with the automated tests 251 htmlReport, _ := scanReport.ToHTML() 252 htmlReportPath := filepath.Join(ReportsDirectory, "piper_checkmarx_report.html") 253 // Ensure reporting directory exists 254 if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil { 255 return reportPaths, errors.Wrapf(err, "failed to create report directory") 256 } 257 if err := utils.FileWrite(htmlReportPath, htmlReport, 0666); err != nil { 258 log.SetErrorCategory(log.ErrorConfiguration) 259 return reportPaths, errors.Wrapf(err, "failed to write html report") 260 } 261 reportPaths = append(reportPaths, piperutils.Path{Name: "Checkmarx Report", Target: htmlReportPath}) 262 263 // JSON reports are used by step pipelineCreateSummary in order to e.g. prepare an issue creation in GitHub 264 // ignore JSON errors since structure is in our hands 265 jsonReport, _ := scanReport.ToJSON() 266 if exists, _ := utils.DirExists(reporting.StepReportDirectory); !exists { 267 err := utils.MkdirAll(reporting.StepReportDirectory, 0777) 268 if err != nil { 269 return reportPaths, errors.Wrap(err, "failed to create reporting directory") 270 } 271 } 272 if err := utils.FileWrite(filepath.Join(reporting.StepReportDirectory, fmt.Sprintf("checkmarxExecuteScan_sast_%v.json", reportShaCheckmarx([]string{projectName, projectID}))), jsonReport, 0666); err != nil { 273 return reportPaths, errors.Wrapf(err, "failed to write json report") 274 } 275 // we do not add the json report to the overall list of reports for now, 276 // since it is just an intermediary report used as input for later 277 // and there does not seem to be real benefit in archiving it. 278 279 return reportPaths, nil 280 } 281 282 func reportShaCheckmarx(parts []string) string { 283 reportShaData := []byte(strings.Join(parts, ",")) 284 return fmt.Sprintf("%x", sha1.Sum(reportShaData)) 285 }