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