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  }