1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-09-16 09:26:22 +02:00

feat(CxOne): Scan summary in PR (#5464)

This commit is contained in:
Adrien
2025-09-01 09:35:22 +02:00
committed by GitHub
parent 377ca9ed6e
commit 979b798115
4 changed files with 145 additions and 5 deletions

View File

@@ -21,6 +21,7 @@ import (
piperGithub "github.com/SAP/jenkins-library/pkg/github"
piperHttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/orchestrator"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/reporting"
"github.com/SAP/jenkins-library/pkg/telemetry"
@@ -451,11 +452,25 @@ func (c *checkmarxOneExecuteScanHelper) CreateScanRequest(incremental bool, uplo
}
branch := c.config.Branch
if len(branch) == 0 && len(c.config.GitBranch) > 0 {
cicdOrch, err := orchestrator.GetOrchestratorConfigProvider(nil)
if err == nil {
log.Entry().Warn("Could not identify orchestrator")
}
if len(branch) == 0 && len(c.config.GitBranch) > 0 && c.config.GitBranch != "n/a" {
branch = c.config.GitBranch
} else if len(branch) == 0 && (len(c.config.GitBranch) == 0 || c.config.GitBranch == "n/a") { // use the branch from the orchestrator by default
cicdBranch := cicdOrch.Branch()
if cicdBranch != "n/a" {
branch = cicdBranch
log.Entry().Infof("CxOne scan branch was automatically set to : %v", branch)
} else {
log.Entry().Info("Could not retrieve branch name from orchestrator")
}
}
if len(c.config.PullRequestName) > 0 {
branch = fmt.Sprintf("%v-%v", c.config.PullRequestName, c.config.Branch)
} else if cicdOrch.IsPullRequest() && cicdOrch.PullRequestConfig().Branch != "n/a" {
branch = fmt.Sprintf("PR%v-%v", cicdOrch.PullRequestConfig().Key, cicdOrch.PullRequestConfig().Branch)
}
sastConfigString = fmt.Sprintf("Cx1 Branch name %v, ", branch) + sastConfigString
@@ -533,10 +548,107 @@ func (c *checkmarxOneExecuteScanHelper) PollScanStatus(scan *checkmarxOne.Scan)
return nil
}
func (c *checkmarxOneExecuteScanHelper) PostScanSummaryInPullRequest(detailedResults *map[string]interface{}, insecure bool) error {
cicdOrch, err := orchestrator.GetOrchestratorConfigProvider(nil)
if err != nil {
return fmt.Errorf("Failed to get orchestrator config provider: %s", err)
}
isPullRequest := cicdOrch.IsPullRequest()
pullRequestId := cicdOrch.PullRequestConfig().Key
var owner, repository string
if len(c.config.Repository) == 0 || len(c.config.Owner) == 0 {
log.Entry().Debug("No repository or owner configured, trying to get it from orchestrator")
repoUrl := cicdOrch.RepoURL()
if repoUrl != "n/a" {
parsedURL, err := url.Parse(repoUrl)
if err != nil {
return fmt.Errorf("failed to parse repository URL %s: %s", repoUrl, err)
}
pathParts := strings.Split(strings.TrimSuffix(parsedURL.Path, ".git"), "/")
if len(pathParts) >= 2 {
if len(c.config.Owner) == 0 {
owner = pathParts[len(pathParts)-2]
}
if len(c.config.Repository) == 0 {
repository = pathParts[len(pathParts)-1]
}
log.Entry().Infof("Found repository %s and owner %s from orchestrator", c.config.Repository, c.config.Owner)
} else {
return fmt.Errorf("failed to extract owner and repository from URL %s", repoUrl)
}
} else {
log.Entry().Debug("Could not retrieve repository URL from orchestrator")
}
} else {
owner = c.config.Owner
repository = c.config.Repository
log.Entry().Debug("Using Owner and Repository from configuration: " + owner + "/" + repository)
}
log.Entry().Debugf("Parameters for PR summary: ScanSummaryInPullRequest: %t, isPullRequest: %t, pullRequestId: %s, PullRequestName: %s, GithubAPIURL: %s, GithubToken: %s, Owner: %s, Repository: %s", c.config.ScanSummaryInPullRequest, isPullRequest, pullRequestId, c.config.PullRequestName, c.config.GithubAPIURL, c.config.GithubToken, owner, repository)
if c.config.ScanSummaryInPullRequest && isPullRequest && pullRequestId != "n/a" && len(c.config.GithubToken) > 0 && len(c.config.GithubAPIURL) > 0 && len(owner) > 0 && len(repository) > 0 {
ghIssues := c.utils.GetIssueService()
log.Entry().Debugf("Creating/updating GitHub issue with check results with PR: %s, GithubAPIURL: %s, Owner: %s, Repository: %s", c.config.PullRequestName, c.config.GithubAPIURL, owner, repository)
scanReportOverview := checkmarxOne.CreateJSONHeaderReport(detailedResults)
var criticalSeverityString, highSeverityString, mediumSeverityString, lowSeverityString string
for _, finding := range *scanReportOverview.Findings {
switch finding.ClassificationName {
case "Critical":
criticalSeverityString = fmt.Sprintf("%d", finding.Total-*finding.Audited)
case "High":
highSeverityString = fmt.Sprintf("%d", finding.Total-*finding.Audited)
case "Medium":
mediumSeverityString = fmt.Sprintf("%d", finding.Total-*finding.Audited)
case "Low":
if finding.LowPerQuery != nil {
for _, lowFinding := range *finding.LowPerQuery {
if c.config.VulnerabilityThresholdLowPerQuery {
lowAuditedRequiredPerQuery := min(int(math.Ceil(float64(lowFinding.Total)*float64(c.config.VulnerabilityThresholdLow)/100.0)), c.config.VulnerabilityThresholdLowPerQueryMax)
lowSeverityString = fmt.Sprintf("%s%d %s (%d audits / %d required) <br>", lowSeverityString, lowFinding.Total-lowFinding.Audited, lowFinding.QueryName, lowFinding.Audited, lowAuditedRequiredPerQuery)
} else {
lowSeverityString = fmt.Sprintf("%s%d %s<br>", lowSeverityString, lowFinding.Total-lowFinding.Audited, lowFinding.QueryName)
}
}
}
}
}
var scanIcon string
if insecure {
scanIcon = ":x:"
} else {
scanIcon = ":white_check_mark:"
}
comment := &github.IssueComment{
Body: github.Ptr(fmt.Sprintf(`# %s Checkmarx %s scan completed
**Project**: %s
**ScanId**: %s
**Preset**: %s
Severity | Number of open findings
--- | ---
:bangbang: Critical | %s
:red_circle: High | %s
:orange_circle: Medium | %s
:yellow_circle: Low | %s
[Go to the scan results](%s)
`, scanIcon, strings.ToLower(scanReportOverview.ScanType), c.Project.Name, scanReportOverview.ScanID, scanReportOverview.Preset, criticalSeverityString, highSeverityString, mediumSeverityString, lowSeverityString, scanReportOverview.DeepLink)),
}
pullRequestNumber, err := strconv.Atoi(pullRequestId)
if err != nil {
return fmt.Errorf("failed to parse int from pull request name %s: %s", c.config.PullRequestName, err)
}
_, _, err = ghIssues.CreateComment(c.ctx, owner, repository, pullRequestNumber, comment)
if err != nil {
return fmt.Errorf("failed to create GitHub issue comment: %s", err)
}
log.Entry().Infof("Created GitHub issue comment for project %v", c.Project.Name)
} else {
log.Entry().Debug("Skipping GitHub issue comment creation, no pull request or GitHub configuration provided")
}
return nil
}
func (c *checkmarxOneExecuteScanHelper) CheckCompliance(scan *checkmarxOne.Scan, detailedResults *map[string]interface{}) error {
links := []piperutils.Path{{Target: (*detailedResults)["DeepLink"].(string), Name: "Checkmarx One Web UI"}}
insecure := false
var insecureResults []string
var neutralResults []string
@@ -545,6 +657,14 @@ func (c *checkmarxOneExecuteScanHelper) CheckCompliance(scan *checkmarxOne.Scan,
insecure, insecureResults, neutralResults = c.enforceThresholds(detailedResults)
scanReport := checkmarxOne.CreateCustomReport(detailedResults, insecureResults, neutralResults)
// Create scan summary comment in PR
if c.config.ScanSummaryInPullRequest {
err := c.PostScanSummaryInPullRequest(detailedResults, insecure)
if err != nil {
log.Entry().Errorf("failed to post scan summary in pull request: %s", err)
}
}
if insecure && c.config.CreateResultIssue && len(c.config.GithubToken) > 0 && len(c.config.GithubAPIURL) > 0 && len(c.config.Owner) > 0 && len(c.config.Repository) > 0 {
log.Entry().Debug("Creating/updating GitHub issue with check results")
gh := reporting.GitHub{
@@ -1144,7 +1264,7 @@ func (c *checkmarxOneExecuteScanHelper) enforceThresholds(results *map[string]in
for lowQuery, resultsLowQuery := range lowPerQueryMap {
lowAuditedPerQuery := resultsLowQuery["Confirmed"] + resultsLowQuery["NotExploitable"]
lowOverallPerQuery := resultsLowQuery["Issues"]
lowAuditedRequiredPerQuery := int(math.Ceil(float64(lowOverallPerQuery) * float64(cxLowThreshold) / 100.0))
lowAuditedRequiredPerQuery := min(int(math.Ceil(float64(lowOverallPerQuery)*float64(cxLowThreshold)/100.0)), cxLowThresholdPerQueryMax)
if lowAuditedPerQuery < lowAuditedRequiredPerQuery && lowAuditedPerQuery < cxLowThresholdPerQueryMax {
insecure = true
msgSeperator := "|"

View File

@@ -31,6 +31,7 @@ type checkmarxOneExecuteScanOptions struct {
GeneratePdfReport bool `json:"generatePdfReport,omitempty"`
GithubAPIURL string `json:"githubApiUrl,omitempty"`
GithubToken string `json:"githubToken,omitempty"`
ScanSummaryInPullRequest bool `json:"scanSummaryInPullRequest,omitempty"`
Incremental bool `json:"incremental,omitempty"`
Owner string `json:"owner,omitempty"`
GitBranch string `json:"gitBranch,omitempty"`
@@ -403,6 +404,7 @@ func addCheckmarxOneExecuteScanFlags(cmd *cobra.Command, stepConfig *checkmarxOn
cmd.Flags().BoolVar(&stepConfig.GeneratePdfReport, "generatePdfReport", true, "Whether to generate a PDF report of the analysis results or not")
cmd.Flags().StringVar(&stepConfig.GithubAPIURL, "githubApiUrl", `https://api.github.com`, "Set the GitHub API URL.")
cmd.Flags().StringVar(&stepConfig.GithubToken, "githubToken", os.Getenv("PIPER_githubToken"), "GitHub personal access token as per https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line")
cmd.Flags().BoolVar(&stepConfig.ScanSummaryInPullRequest, "scanSummaryInPullRequest", false, "Whether the scan summary shall be added to the pull request as a comment or not. This is only applied if the step is executed in a pull request context. githubToken and githubApiUrl parameters must be set to allow the step to create the comment.")
cmd.Flags().BoolVar(&stepConfig.Incremental, "incremental", true, "Whether incremental scans are to be applied which optimizes the scan time but might reduce detection capabilities. Therefore full scans are still required from time to time and should be scheduled via `fullScansScheduled` and `fullScanCycle`")
cmd.Flags().StringVar(&stepConfig.Owner, "owner", os.Getenv("PIPER_owner"), "Set the GitHub organization.")
cmd.Flags().StringVar(&stepConfig.GitBranch, "gitBranch", os.Getenv("PIPER_gitBranch"), "Set the GitHub repository branch.")
@@ -551,6 +553,15 @@ func checkmarxOneExecuteScanMetadata() config.StepData {
Aliases: []config.Alias{{Name: "access_token"}},
Default: os.Getenv("PIPER_githubToken"),
},
{
Name: "scanSummaryInPullRequest",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "bool",
Mandatory: false,
Aliases: []config.Alias{},
Default: false,
},
{
Name: "incremental",
ResourceRef: []config.ResourceReference{},

View File

@@ -106,6 +106,14 @@ spec:
- type: vaultSecret
default: github
name: githubVaultSecretName
- name: scanSummaryInPullRequest
description: Whether the scan summary shall be added to the pull request as a comment or not. This is only applied if the step is executed in a pull request context. githubToken and githubApiUrl parameters must be set to allow the step to create the comment.
type: bool
scope:
- PARAMETERS
- STAGES
- STEPS
default: false
- name: incremental
type: bool
description: Whether incremental scans are to be applied which optimizes the scan time but might reduce detection capabilities. Therefore full scans are still required from time to time and should be scheduled via `fullScansScheduled` and `fullScanCycle`

View File

@@ -7,6 +7,7 @@ import groovy.transform.Field
void call(Map parameters = [:]) {
List credentials = [[type: 'usernamePassword', id: 'checkmarxOneCredentialsId', env: ['PIPER_clientId', 'PIPER_clientSecret']],
[type: 'token', id: 'checkmarxOneAPIKey', env: ['PIPER_APIKey']]]
[type: 'token', id: 'checkmarxOneAPIKey', env: ['PIPER_APIKey']],
[type: 'token', id: 'githubTokenCredentialsId', env: ['PIPER_githubToken']]]
piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials)
}