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  }