diff --git a/docs/Bug-Trackers-and-Feedback-Channels.md b/docs/Bug-Trackers-and-Feedback-Channels.md index 05394152a..2d54f01bb 100644 --- a/docs/Bug-Trackers-and-Feedback-Channels.md +++ b/docs/Bug-Trackers-and-Feedback-Channels.md @@ -497,7 +497,7 @@ azure: ## GitLab Issues * GitLab Issues leverages the same configuration as specified for WebHook listeners → API token (**token**) and valid urls are required -``` +```yaml gitlab: webhook-token: XXXX token: xxx @@ -505,6 +505,40 @@ gitlab: api-url: https://gitlab.com/api/v4/ false-positive-label: false-positive block-merge: true + fields: + - type: result + name: application + - type: result + name: project +``` +#### Fields +Every value mentioned in fields will be added in Labels. +* **type** + * **cx-scan**: Used to map specific Checkmarx Scan Custom Field values + * **cx-sca**: Used to map specific Checkmarx SCA Scan tags values + * **static**: Used for static values (specifically requires a default-value to be provided) + * **result**: Used to map known values from Checkmarx results or repository/scan request details. Refer to the Result values below. + +* **name**: If cx-scan or cx-sca reflects the type, it is the name of the custom field within Checkmarx SAST or key of tag in case o SCA +* **default-value** Static value if no value can be determined for the respective field (Optional) +* If **result** is provided as type, the name must be one of the following: + +``` +application - Command line option --app +project - Command line option --cx-project +namespace - Command line option --namespace +repo-name - Command line option --repo-name +repo-url - Command line option --repo-url +branch - Command line option --branch +severity - Severity of issue in Checkmarx +category - Category of issue in Checkmarx +cwe - CWE of issue in Checkmarx +recommendation - Recommendation details based on Mitre/Custom Wiki +loc - csv of lines of code +issue-link - Direct link to issue within Checkmarx +filename - Filename provided by Checkmarx issue +language - Language provided by Checkmarx issue +similarity-id - Cx Similarity ID ``` [[/Images/bug2.png|Screenshot of GitLab issue]] @@ -591,6 +625,40 @@ github: api-url: https://api.github.com/repos/ false-positive-label: false-positive block-merge: true + fields: + - type: result + name: application + - type: result + name: project +``` +#### Fields +Every value mentioned in fields will be added in Labels. +* **type** + * **cx-scan**: Used to map specific Checkmarx Scan Custom Field values + * **cx-sca**: Used to map specific Checkmarx SCA Scan tags values + * **static**: Used for static values (specifically requires a default-value to be provided) + * **result**: Used to map known values from Checkmarx results or repository/scan request details. Refer to the Result values below. + +* **name**: If cx-scan or cx-sca reflects the type, it is the name of the custom field within Checkmarx SAST or key of tag in case o SCA +* **default-value** Static value if no value can be determined for the respective field (Optional) +* If **result** is provided as type, the name must be one of the following: + +``` +application - Command line option --app +project - Command line option --cx-project +namespace - Command line option --namespace +repo-name - Command line option --repo-name +repo-url - Command line option --repo-url +branch - Command line option --branch +severity - Severity of issue in Checkmarx +category - Category of issue in Checkmarx +cwe - CWE of issue in Checkmarx +recommendation - Recommendation details based on Mitre/Custom Wiki +loc - csv of lines of code +issue-link - Direct link to issue within Checkmarx +filename - Filename provided by Checkmarx issue +language - Language provided by Checkmarx issue +similarity-id - Cx Similarity ID ``` [[/Images/bug4.png|Screenshot of GitHub issue]] diff --git a/docs/Configuration.md b/docs/Configuration.md index 6f5851c2c..8619534b7 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -838,6 +838,11 @@ github: max-delay : comment-update: false zero-vulnerability-summary: true + fields: + - type: result + name: application + - type: result + name: project ``` | Configuration | Default | Description | @@ -853,8 +858,10 @@ github: | `max-delay` | | When Secondary rate limit is hit, it will delay each API call for issue creation(Mininum value should be 3) | | `comment-update` | true | if false, will create a new comment for every scan | | `zero-vulnerability-summary` | false | if true, will not comment in PR decoration any details for scans as vulnerabilities are zero. | +| `fields` | | Refer page: [Bug-Trackers-and-Feedback-Channels Chapter Github Fields](https://github.com/checkmarx-ltd/cx-flow/wiki/Bug-Trackers-and-Feedback-Channels#githubfields) | **Note**: A service account is required with access to the repositories that will be scanned, pull requests that will be commented on, and GitHub issues that will be created/updated. + ### GitLab ```yaml gitlab: @@ -866,6 +873,11 @@ gitlab: block-merge: true comment-update: false zero-vulnerability-summary: true + fields: + - type: result + name: application + - type: result + name: project ``` | Configuration | Default | Description | @@ -879,6 +891,7 @@ gitlab: | `scan-submitted-comment` | true | Comment on Merge Request with "Scan submitted (or not submitted) to Checkmarx ...". | | `comment-update` | true | if false, will create a new comment for every scan | | `zero-vulnerability-summary` | false | if true, will not comment in PR decoration any details for scans as vulnerabilities are zero. | +| `fields` | | Refer page: [Bug-Trackers-and-Feedback-Channels Chapter Gitlab Fields](https://github.com/checkmarx-ltd/cx-flow/wiki/Bug-Trackers-and-Feedback-Channels#gitlabfields) | **Note**: A service account is required with access to the repositories that are going to be scanned, pull requests that are commented on, and GitLab issues that are created/updated. diff --git a/src/main/java/com/checkmarx/flow/config/GitHubProperties.java b/src/main/java/com/checkmarx/flow/config/GitHubProperties.java index 94965345f..97e0636d0 100644 --- a/src/main/java/com/checkmarx/flow/config/GitHubProperties.java +++ b/src/main/java/com/checkmarx/flow/config/GitHubProperties.java @@ -1,11 +1,14 @@ package com.checkmarx.flow.config; +import com.checkmarx.flow.dto.Field; +import com.checkmarx.flow.dto.LabelField; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; +import java.util.List; import java.util.Map; @Component @@ -40,6 +43,13 @@ public class GitHubProperties extends RepoProperties { @Setter private Map issueslabel; + @Getter + @Setter + private boolean commentUpdate =true; + @Getter + @Setter + private List fields; + public String getMergeNoteUri(String namespace, String repo, String mergeId){ String format = "%s/%s/%s/issues/%s/comments"; diff --git a/src/main/java/com/checkmarx/flow/config/GitLabProperties.java b/src/main/java/com/checkmarx/flow/config/GitLabProperties.java index b6cff313a..44cf2953a 100644 --- a/src/main/java/com/checkmarx/flow/config/GitLabProperties.java +++ b/src/main/java/com/checkmarx/flow/config/GitLabProperties.java @@ -1,11 +1,13 @@ package com.checkmarx.flow.config; +import com.checkmarx.flow.dto.LabelField; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; +import java.util.List; import java.util.Map; @Component @@ -30,7 +32,9 @@ public class GitLabProperties extends RepoProperties { @Getter @Setter private boolean commentUpdate =true; - + @Getter + @Setter + private List fields; public String getGitUri(String namespace, String repo){ diff --git a/src/main/java/com/checkmarx/flow/custom/GitHubIssueTracker.java b/src/main/java/com/checkmarx/flow/custom/GitHubIssueTracker.java index 8cf28c4f5..d4c1ee2c1 100644 --- a/src/main/java/com/checkmarx/flow/custom/GitHubIssueTracker.java +++ b/src/main/java/com/checkmarx/flow/custom/GitHubIssueTracker.java @@ -4,7 +4,9 @@ import com.checkmarx.flow.config.FlowProperties; import com.checkmarx.flow.config.GitHubProperties; import com.checkmarx.flow.config.ScmConfigOverrider; +import com.checkmarx.flow.constants.FlowConstants; import com.checkmarx.flow.dto.Issue; +import com.checkmarx.flow.dto.LabelField; import com.checkmarx.flow.dto.ScanRequest; import com.checkmarx.flow.dto.github.LabelsItem; import com.checkmarx.flow.exception.MachinaException; @@ -13,6 +15,7 @@ import com.checkmarx.flow.utils.HTMLHelper; import com.checkmarx.flow.utils.ScanUtils; import com.checkmarx.sdk.dto.ScanResults; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.ThreadUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.json.JSONException; @@ -26,7 +29,10 @@ import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.stream.Collectors; @Service("GitHub") public class GitHubIssueTracker implements IssueTracker { @@ -287,7 +293,7 @@ private JSONObject getJSONUpdateIssue(ScanResults.XIssue resultIssue, ScanReques String fileUrl = ScanUtils.getFileUrl(request, resultIssue.getFilename()); String body = HTMLHelper.getMDBody(resultIssue, request.getBranch(), fileUrl, flowProperties,properties.getMaxDescriptionLength()); String title = getXIssueKey(resultIssue, request); - String[] label = getString(resultIssue); + String[] label = getString(request,resultIssue); try { @@ -314,7 +320,7 @@ private JSONObject getJSONCreateIssue(ScanResults.XIssue resultIssue, ScanReques String fileUrl = ScanUtils.getFileUrl(request, resultIssue.getFilename()); String body = HTMLHelper.getMDBody(resultIssue, request.getBranch(), fileUrl, flowProperties,properties.getMaxDescriptionLength()); String title = HTMLHelper.getScanRequestIssueKeyWithDefaultProductValue(request, this, resultIssue); - String[] label = getString(resultIssue); + String[] label = getString(request,resultIssue); try { requestBody.put("title", title); @@ -329,17 +335,297 @@ private JSONObject getJSONCreateIssue(ScanResults.XIssue resultIssue, ScanReques return requestBody; } - private String[] getString(ScanResults.XIssue resultIssue) { + private String[] getString(ScanRequest request, ScanResults.XIssue issue) { + List value = new ArrayList<>(); String[] strArray = new String[]{}; try { - Map findingsPerSeverity = properties.getIssueslabel(); - for (Map.Entry entry : findingsPerSeverity.entrySet()) { - if(resultIssue.getSeverity().equalsIgnoreCase(entry.getKey().toString()) || resultIssue.getSeverity().toLowerCase(Locale.ROOT).contains(entry.getKey().toString().toLowerCase(Locale.ROOT))){ + if(properties.getIssueslabel()!=null){ + Map findingsPerSeverity = properties.getIssueslabel(); + for (Map.Entry entry : findingsPerSeverity.entrySet()) { + if (issue.getSeverity().equalsIgnoreCase(entry.getKey().toString()) || issue.getSeverity().toLowerCase(Locale.ROOT).contains(entry.getKey().toString().toLowerCase(Locale.ROOT))) { //converting using String.split() method with whitespace as a delimiter - strArray = entry.getValue().split(","); - break; + value=Arrays.stream(entry.getValue().split(",")).collect(Collectors.toList()); + break; + } } } + + //adding fields in labels + for (LabelField f : properties.getFields()) { + String fieldType = f.getType(); + String fieldName; + if (ScanUtils.empty(fieldType)) { + log.warn("Field type not supplied. Using 'result' by default."); + // use default = result + fieldType = "result"; + } + Map addDetails = null; + Map scanCustomFields = null; + String scanScaTags = null; + String scanCustomFieldsValue = null; + if (Objects.nonNull(issue.getAdditionalDetails()) && Objects.nonNull((Map) issue.getAdditionalDetails().get("scanCustomFields"))) { + addDetails = issue.getAdditionalDetails(); + scanCustomFields = (Map) addDetails.get("scanCustomFields"); + scanCustomFieldsValue = scanCustomFields.get(f.getName()); + } + if (Objects.nonNull(issue.getScaDetails()) && Objects.nonNull(issue.getScaDetails().get(0).getScanTags())) { + scanScaTags = (String) issue.getScaDetails().get(0).getScanTags().get(f.getName()); + } + + switch (fieldType) { + case "cx-scan"://cx-scan, cx-sca, sca-results, static, result + log.debug("Checkmarx scan custom field {}", f.getName()); + if (scanCustomFieldsValue != null) { + log.debug("Checkmarx scan custom field"); + value.add(f.getName() + ":" + scanCustomFieldsValue); + log.debug("Cx Scan Field value: {}", value); + if (ScanUtils.empty(value) && !ScanUtils.empty(f.getDefaultValue())) { + value.add(f.getName() + ":" + f.getDefaultValue()); + log.debug("default Value is {}", value); + } + } else { + log.debug("No value found for {}", f.getName()); + value.add(""); + } + break; + case "cx-sca": + log.debug("SCA scan Tags Key name {}", f.getName()); + if (scanScaTags != null) { + value.add(f.getName() + ":" + scanScaTags); + log.debug("SCA scan Field value: {}", value); + if (ScanUtils.empty(value) && !ScanUtils.empty(f.getDefaultValue())) { + value.add(f.getName() + ":" + f.getDefaultValue()); + log.debug(" default Value is {}", value); + } + } else { + log.debug("No value found for {}", f.getName()); + value.add(""); + } + break; + case "sca-results": + if (issue.getScaDetails() == null) { + log.debug("Sca details not available"); + break; + } + fieldName = f.getName(); + switch (fieldName) { + case "package-name": + log.debug("package-name: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().getId()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getVulnerabilityPackage().getId()); + break; + case "current-version": + log.debug("current-version: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().getVersion()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getVulnerabilityPackage().getVersion()); + break; + case "fixed-version": + log.debug("fixed-version: {}", issue.getScaDetails().get(0).getFinding().getFixResolutionText()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getFinding().getFixResolutionText()); + break; + case "newest-version": + log.debug(issue.getScaDetails().get(0).getVulnerabilityPackage().getNewestVersion()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getVulnerabilityPackage().getNewestVersion()); + break; + case "locations": + List locations = issue.getScaDetails().get(0).getVulnerabilityPackage().getLocations(); + String location = null; + for (String l : locations + ) { + location = l + ","; + } + log.debug("locations: {}", location); + assert location != null; + value.add(f.getName() + ":" + location.substring(0, location.length() - 1)); + break; + case "dev-dependency": + log.debug("dev-dependency: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().isIsDevelopmentDependency()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getVulnerabilityPackage().isIsDevelopmentDependency()).toUpperCase()); + break; + case "direct-dependency": + log.debug("direct-dependency: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().isIsDirectDependency()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getVulnerabilityPackage().isIsDirectDependency()).toUpperCase()); + break; + case "risk-score": + log.debug("risk score: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().getRiskScore()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getVulnerabilityPackage().getRiskScore())); + break; + case "outdated": + log.debug("outdated: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().isOutdated()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getVulnerabilityPackage().isOutdated()).toUpperCase()); + break; + case "violates-policy": + log.debug("Violates-Policy: {}", issue.getScaDetails().get(0).getFinding().isViolatingPolicy()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getFinding().isViolatingPolicy()).toUpperCase()); + + } + break; + case "static": + log.debug("Static value {} - {}", f.getName(), f.getDefaultValue()); + value.add(f.getDefaultValue()); + break; + default: //result + fieldName = f.getName(); + if (fieldName == null) { + log.warn("Field name not supplied. Skipping."); + /* there is no default, move on to the next field */ + continue; + } + /*known values we can use*/ + switch (fieldName) { + case "application": + log.debug("application: {}", request.getApplication()); + if(request.getApplication()!=null){ + value.add(f.getName() + ":" + request.getApplication()); + }else{ + value.add(f.getName() + ":" +" "); + } + break; + case "project": + log.debug("project: {}", request.getProject()); + value.add(f.getName() + ":" + request.getProject()); + break; + case "namespace": + log.debug("namespace: {}", request.getNamespace()); + value.add(f.getName() + ":" + request.getNamespace()); + break; + case "repo-name": + log.debug("repo-name: {}", request.getRepoName()); + value.add(f.getName() + ":" + request.getRepoName()); + break; + case "repo-url": + log.debug("repo-url: {}", request.getRepoUrl()); + value.add(f.getName() + ":" + request.getRepoUrl()); + break; + case "branch": + log.debug("branch: {}", request.getBranch()); + value.add(f.getName() + ":" + request.getBranch()); + break; + case "severity": + if (issue.getScaDetails() != null) { + log.debug("severity: {}", issue.getScaDetails().get(0).getFinding().getSeverity()); + value.add(f.getName() + ":" + ScanUtils.toProperCase(String.valueOf(issue.getScaDetails().get(0).getFinding().getSeverity()))); + } else { + log.debug("severity: {}", issue.getSeverity()); + value.add(f.getName() + ":" + ScanUtils.toProperCase(issue.getSeverity())); + } + break; + case "category": + log.debug("category: {}", issue.getVulnerability()); + value.add(f.getName() + ":" + issue.getVulnerability()); + break; + case "cwe": + log.debug("cwe: {}", issue.getCwe()); + value.add(f.getName() + ":" + issue.getCwe()); + break; + case "cve": + if (issue.getScaDetails() != null) { + log.debug("cve: {}", issue.getScaDetails().get(0).getFinding().getId()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getFinding().getId()); + } else { + log.debug("cve: {}", issue.getCve()); + value.add(f.getName() + ":" + issue.getCve()); + } + break; + case "system-date": + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDateTime now = LocalDateTime.now(); + value.add(f.getName() + ":" + dtf.format(now)); + log.debug("system date: {}", value); + break; + case "recommendation": + StringBuilder recommendation = new StringBuilder(); + if (issue.getLink() != null && !issue.getLink().isEmpty()) { + recommendation.append("Checkmarx Link: ").append(issue.getLink()).append(HTMLHelper.CRLF); + } + if (!ScanUtils.anyEmpty(flowProperties.getMitreUrl(), issue.getCwe())) { + recommendation.append("Mitre Details: ").append(String.format(flowProperties.getMitreUrl(), issue.getCwe())).append(HTMLHelper.CRLF); + } + if (!ScanUtils.empty(flowProperties.getWikiUrl())) { + recommendation.append("Guidance: ").append(flowProperties.getWikiUrl()).append(HTMLHelper.CRLF); + } + value.add(f.getName() + ":" + recommendation.toString()); + break; + case "loc": + if (issue.getDetails() != null) { + List lines = issue.getDetails().entrySet() + .stream() + .filter(x -> x.getKey() != null && x.getValue() != null && !x.getValue().isFalsePositive()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + if (!lines.isEmpty()) { + Collections.sort(lines); + value.add(f.getName() + ":" + StringUtils.join(lines, ",")); + log.debug("loc: {}", value); + } + } + break; + case "not-exploitable": + List fpLines; + if (issue.getDetails() != null) { + fpLines = issue.getDetails().entrySet() + .stream() + .filter(x -> x.getKey() != null && x.getValue() != null && x.getValue().isFalsePositive()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + if (!fpLines.isEmpty()) { + Collections.sort(fpLines); + value.add(f.getName() + ":" + StringUtils.join(fpLines, ",")); + log.debug("loc: {}", value); + } + } + break; + case "site": + log.debug("site: {}", request.getSite()); + value.add(f.getName() + ":" + request.getSite()); + break; + case "issue-link": + if (issue.getScaDetails() != null) { + log.debug("issue-link: {}", issue.getScaDetails().get(0).getVulnerabilityLink()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getVulnerabilityLink()); + } else { + log.debug("issue-link: {}", issue.getLink()); + value.add(f.getName() + ":" + issue.getLink()); + } + break; + case "filename": + log.debug("filename: {}", issue.getFilename()); + value.add(f.getName() + ":" + issue.getFilename()); + break; + case "language": + log.debug("language: {}", issue.getLanguage()); + value.add(f.getName() + ":" + issue.getLanguage()); + break; + case "similarity-id": + log.debug("similarity-id: {}", issue.getSimilarityId()); + value.add(f.getName() + ":" + issue.getSimilarityId()); + break; + case "comment": + StringBuilder comments = new StringBuilder(); + String commentFmt = "[Line %s]: [%s]".concat(HTMLHelper.CRLF); + if (issue.getDetails() != null) { + issue.getDetails().entrySet() + .stream() + .filter(x -> x.getKey() != null && x.getValue() != null && x.getValue().getComment() != null && !x.getValue().getComment().isEmpty()) + .forEach(c -> comments.append(String.format(commentFmt, c.getKey(), c.getValue().getComment()))); + value.add(f.getName() + ":" + comments.toString()); + } + break; + default: + log.warn("field value for {} not found", f.getName()); + value.add(""); + } + /*If the value is missing, check if a default value was specified*/ + if (ScanUtils.empty(value)) { + log.debug("Value is empty, defaulting to configured default (if applicable)"); + if (!ScanUtils.empty(f.getDefaultValue())) { + value.add(f.getName() + ":" + f.getDefaultValue()); + log.debug("Default value is {}", value); + } + } + break; + } + } + strArray = value.toArray(new String[0]); + } catch (Exception e) { return strArray; } diff --git a/src/main/java/com/checkmarx/flow/custom/GitLabIssueTracker.java b/src/main/java/com/checkmarx/flow/custom/GitLabIssueTracker.java index 6d35adfe4..f28cc2092 100644 --- a/src/main/java/com/checkmarx/flow/custom/GitLabIssueTracker.java +++ b/src/main/java/com/checkmarx/flow/custom/GitLabIssueTracker.java @@ -5,12 +5,14 @@ import com.checkmarx.flow.config.GitLabProperties; import com.checkmarx.flow.config.ScmConfigOverrider; import com.checkmarx.flow.dto.Issue; +import com.checkmarx.flow.dto.LabelField; import com.checkmarx.flow.dto.ScanRequest; import com.checkmarx.flow.dto.gitlab.Note; import com.checkmarx.flow.exception.MachinaException; import com.checkmarx.flow.utils.HTMLHelper; import com.checkmarx.flow.utils.ScanUtils; import com.checkmarx.sdk.dto.ScanResults; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.json.JSONArray; import org.json.JSONException; @@ -25,7 +27,10 @@ import java.net.URI; import java.net.URISyntaxException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.stream.Collectors; @Service("GitLab") public class GitLabIssueTracker implements IssueTracker { @@ -363,7 +368,7 @@ private JSONObject getJSONUpdateIssue(ScanResults.XIssue resultIssue, ScanReques String fileUrl = getFileUrl(request, resultIssue.getFilename()); String body = HTMLHelper.getMDBody(resultIssue, request.getBranch(), fileUrl, flowProperties, max_desc_length); String title = getXIssueKey(resultIssue, request); - String label = getString(resultIssue); + String label = getString(request,resultIssue); try { requestBody.put("title", title); @@ -388,7 +393,7 @@ private JSONObject getJSONCreateIssue(ScanResults.XIssue resultIssue, ScanReques String fileUrl = getFileUrl(request, resultIssue.getFilename()); String body = HTMLHelper.getMDBody(resultIssue, request.getBranch(), fileUrl, flowProperties, max_desc_length); String title = HTMLHelper.getScanRequestIssueKeyWithDefaultProductValue(request, this, resultIssue); - String label = getString(resultIssue); + String label = getString(request,resultIssue); try { requestBody.put("title", title); @@ -402,16 +407,302 @@ private JSONObject getJSONCreateIssue(ScanResults.XIssue resultIssue, ScanReques return requestBody; } - private String getString(ScanResults.XIssue resultIssue) { + private String getString(ScanRequest request,ScanResults.XIssue issue) { String label = "NA"; + List value = new ArrayList<>(); try { - Map findingsPerSeverity = properties.getIssueslabel(); - for (Map.Entry entry : findingsPerSeverity.entrySet()) { - if (resultIssue.getSeverity().equalsIgnoreCase(entry.getKey().toString()) || resultIssue.getSeverity().toLowerCase(Locale.ROOT).contains(entry.getKey().toString().toLowerCase(Locale.ROOT))) { - label = entry.getValue(); - break; + if(properties.getIssueslabel()!=null) + { + Map findingsPerSeverity = properties.getIssueslabel(); + for (Map.Entry entry : findingsPerSeverity.entrySet()) { + if (issue.getSeverity().equalsIgnoreCase(entry.getKey().toString()) || issue.getSeverity().toLowerCase(Locale.ROOT).contains(entry.getKey().toString().toLowerCase(Locale.ROOT))) { + label = entry.getValue(); + break; + } + } + } + + for (LabelField f : properties.getFields()) { + String fieldType = f.getType(); + String fieldName; + if (ScanUtils.empty(fieldType)) { + log.warn("Field type not supplied. Using 'result' by default."); + // use default = result + fieldType = "result"; + } + Map addDetails = null; + Map scanCustomFields = null; + String scanScaTags = null; + String scanCustomFieldsValue = null; + if (Objects.nonNull(issue.getAdditionalDetails()) && Objects.nonNull((Map) issue.getAdditionalDetails().get("scanCustomFields"))) { + addDetails = issue.getAdditionalDetails(); + scanCustomFields = (Map) addDetails.get("scanCustomFields"); + scanCustomFieldsValue = scanCustomFields.get(f.getName()); + } + if (Objects.nonNull(issue.getScaDetails()) && Objects.nonNull(issue.getScaDetails().get(0).getScanTags())) { + scanScaTags = (String) issue.getScaDetails().get(0).getScanTags().get(f.getName()); + } + + switch (fieldType) { + case "cx-scan"://cx-scan, cx-sca, sca-results, static, result + log.debug("Checkmarx scan custom field {}", f.getName()); + if (scanCustomFieldsValue != null) { + log.debug("Checkmarx scan custom field"); + value.add(f.getName() + ":" + scanCustomFieldsValue); + log.debug("Cx Scan Field value: {}", value); + if (ScanUtils.empty(value) && !ScanUtils.empty(f.getDefaultValue())) { + value.add(f.getName() + ":" + f.getDefaultValue()); + log.debug("default Value is {}", value); + } + } else { + log.debug("No value found for {}", f.getName()); + value.add(""); + } + break; + case "cx-sca": + log.debug("SCA scan Tags Key name {}", f.getName()); + if (scanScaTags != null) { + value.add(f.getName() + ":" + scanScaTags); + log.debug("SCA scan Field value: {}", value); + if (ScanUtils.empty(value) && !ScanUtils.empty(f.getDefaultValue())) { + value.add(f.getName() + ":" + f.getDefaultValue()); + log.debug(" default Value is {}", value); + } + } else { + log.debug("No value found for {}", f.getName()); + value.add(""); + } + break; + case "sca-results": + if (issue.getScaDetails() == null) { + log.debug("Sca details not available"); + break; + } + fieldName = f.getName(); + switch (fieldName) { + case "package-name": + log.debug("package-name: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().getId()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getVulnerabilityPackage().getId()); + break; + case "current-version": + log.debug("current-version: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().getVersion()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getVulnerabilityPackage().getVersion()); + break; + case "fixed-version": + log.debug("fixed-version: {}", issue.getScaDetails().get(0).getFinding().getFixResolutionText()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getFinding().getFixResolutionText()); + break; + case "newest-version": + log.debug(issue.getScaDetails().get(0).getVulnerabilityPackage().getNewestVersion()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getVulnerabilityPackage().getNewestVersion()); + break; + case "locations": + List locations = issue.getScaDetails().get(0).getVulnerabilityPackage().getLocations(); + String location = null; + for (String l : locations + ) { + location = l + ","; + } + log.debug("locations: {}", location); + assert location != null; + value.add(f.getName() + ":" + location.substring(0, location.length() - 1)); + break; + case "dev-dependency": + log.debug("dev-dependency: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().isIsDevelopmentDependency()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getVulnerabilityPackage().isIsDevelopmentDependency()).toUpperCase()); + break; + case "direct-dependency": + log.debug("direct-dependency: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().isIsDirectDependency()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getVulnerabilityPackage().isIsDirectDependency()).toUpperCase()); + break; + case "risk-score": + log.debug("risk score: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().getRiskScore()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getVulnerabilityPackage().getRiskScore())); + break; + case "outdated": + log.debug("outdated: {}", issue.getScaDetails().get(0).getVulnerabilityPackage().isOutdated()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getVulnerabilityPackage().isOutdated()).toUpperCase()); + break; + case "violates-policy": + log.debug("Violates-Policy: {}", issue.getScaDetails().get(0).getFinding().isViolatingPolicy()); + value.add(f.getName() + ":" + String.valueOf(issue.getScaDetails().get(0).getFinding().isViolatingPolicy()).toUpperCase()); + + } + break; + case "static": + log.debug("Static value {} - {}", f.getName(), f.getDefaultValue()); + value.add(f.getDefaultValue()); + break; + default: //result + fieldName = f.getName(); + if (fieldName == null) { + log.warn("Field name not supplied. Skipping."); + /* there is no default, move on to the next field */ + continue; + } + /*known values we can use*/ + switch (fieldName) { + case "application": + log.debug("application: {}", request.getApplication()); + if(request.getApplication()!=null){ + value.add(f.getName() + ":" + request.getApplication()); + }else{ + value.add(f.getName() + ":" +" "); + } + break; + case "project": + log.debug("project: {}", request.getProject()); + value.add(f.getName() + ":" + request.getProject()); + break; + case "namespace": + log.debug("namespace: {}", request.getNamespace()); + value.add(f.getName() + ":" + request.getNamespace()); + break; + case "repo-name": + log.debug("repo-name: {}", request.getRepoName()); + value.add(f.getName() + ":" + request.getRepoName()); + break; + case "repo-url": + log.debug("repo-url: {}", request.getRepoUrl()); + value.add(f.getName() + ":" + request.getRepoUrl()); + break; + case "branch": + log.debug("branch: {}", request.getBranch()); + value.add(f.getName() + ":" + request.getBranch()); + break; + case "severity": + if (issue.getScaDetails() != null) { + log.debug("severity: {}", issue.getScaDetails().get(0).getFinding().getSeverity()); + value.add(f.getName() + ":" + ScanUtils.toProperCase(String.valueOf(issue.getScaDetails().get(0).getFinding().getSeverity()))); + } else { + log.debug("severity: {}", issue.getSeverity()); + value.add(f.getName() + ":" + ScanUtils.toProperCase(issue.getSeverity())); + } + break; + case "category": + log.debug("category: {}", issue.getVulnerability()); + value.add(f.getName() + ":" + issue.getVulnerability()); + break; + case "cwe": + log.debug("cwe: {}", issue.getCwe()); + value.add(f.getName() + ":" + issue.getCwe()); + break; + case "cve": + if (issue.getScaDetails() != null) { + log.debug("cve: {}", issue.getScaDetails().get(0).getFinding().getId()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getFinding().getId()); + } else { + log.debug("cve: {}", issue.getCve()); + value.add(f.getName() + ":" + issue.getCve()); + } + break; + case "system-date": + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDateTime now = LocalDateTime.now(); + value.add(f.getName() + ":" + dtf.format(now)); + log.debug("system date: {}", value); + break; + case "recommendation": + StringBuilder recommendation = new StringBuilder(); + if (issue.getLink() != null && !issue.getLink().isEmpty()) { + recommendation.append("Checkmarx Link: ").append(issue.getLink()).append(HTMLHelper.CRLF); + } + if (!ScanUtils.anyEmpty(flowProperties.getMitreUrl(), issue.getCwe())) { + recommendation.append("Mitre Details: ").append(String.format(flowProperties.getMitreUrl(), issue.getCwe())).append(HTMLHelper.CRLF); + } + if (!ScanUtils.empty(flowProperties.getWikiUrl())) { + recommendation.append("Guidance: ").append(flowProperties.getWikiUrl()).append(HTMLHelper.CRLF); + } + value.add(f.getName() + ":" + recommendation.toString()); + break; + case "loc": + if (issue.getDetails() != null) { + List lines = issue.getDetails().entrySet() + .stream() + .filter(x -> x.getKey() != null && x.getValue() != null && !x.getValue().isFalsePositive()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + if (!lines.isEmpty()) { + Collections.sort(lines); + value.add(f.getName() + ":" + StringUtils.join(lines, ",")); + log.debug("loc: {}", value); + } + } + break; + case "not-exploitable": + List fpLines; + if (issue.getDetails() != null) { + fpLines = issue.getDetails().entrySet() + .stream() + .filter(x -> x.getKey() != null && x.getValue() != null && x.getValue().isFalsePositive()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + if (!fpLines.isEmpty()) { + Collections.sort(fpLines); + value.add(f.getName() + ":" + StringUtils.join(fpLines, ",")); + log.debug("loc: {}", value); + } + } + break; + case "site": + log.debug("site: {}", request.getSite()); + value.add(f.getName() + ":" + request.getSite()); + break; + case "issue-link": + if (issue.getScaDetails() != null) { + log.debug("issue-link: {}", issue.getScaDetails().get(0).getVulnerabilityLink()); + value.add(f.getName() + ":" + issue.getScaDetails().get(0).getVulnerabilityLink()); + } else { + log.debug("issue-link: {}", issue.getLink()); + value.add(f.getName() + ":" + issue.getLink()); + } + break; + case "filename": + log.debug("filename: {}", issue.getFilename()); + value.add(f.getName() + ":" + issue.getFilename()); + break; + case "language": + log.debug("language: {}", issue.getLanguage()); + value.add(f.getName() + ":" + issue.getLanguage()); + break; + case "similarity-id": + log.debug("similarity-id: {}", issue.getSimilarityId()); + value.add(f.getName() + ":" + issue.getSimilarityId()); + break; + case "comment": + StringBuilder comments = new StringBuilder(); + String commentFmt = "[Line %s]: [%s]".concat(HTMLHelper.CRLF); + if (issue.getDetails() != null) { + issue.getDetails().entrySet() + .stream() + .filter(x -> x.getKey() != null && x.getValue() != null && x.getValue().getComment() != null && !x.getValue().getComment().isEmpty()) + .forEach(c -> comments.append(String.format(commentFmt, c.getKey(), c.getValue().getComment()))); + value.add(f.getName() + ":" + comments.toString()); + } + break; + default: + log.warn("field value for {} not found", f.getName()); + value.add(""); + } + /*If the value is missing, check if a default value was specified*/ + if (ScanUtils.empty(value)) { + log.debug("Value is empty, defaulting to configured default (if applicable)"); + if (!ScanUtils.empty(f.getDefaultValue())) { + value.add(f.getName() + ":" + f.getDefaultValue()); + log.debug("Default value is {}", value); + } + } + break; } } + + + if(properties.getIssueslabel()==null && !value.isEmpty()) + { + label = String.join(",", value); + }else{ + label = label + "," + String.join(",", value); + } } catch (Exception e) { return label; } diff --git a/src/main/java/com/checkmarx/flow/dto/LabelField.java b/src/main/java/com/checkmarx/flow/dto/LabelField.java new file mode 100644 index 000000000..f7c9674c5 --- /dev/null +++ b/src/main/java/com/checkmarx/flow/dto/LabelField.java @@ -0,0 +1,12 @@ +package com.checkmarx.flow.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LabelField { + private String type; + private String name; + private String defaultValue; +}