diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a3e3af..c11a077a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### ⭐ New Features - Support for specifying the parent project using its name and version as an alternative to its ID ([#261](https://github.com/jenkinsci/dependency-track-plugin/issues/261)) - Include artifact name in Publishing Logline ([#264](https://github.com/jenkinsci/dependency-track-plugin/issues/264)) +- Support for Policy Violations ([#130](https://github.com/jenkinsci/dependency-track-plugin/issues/130)) ### 🐞 Bugs Fixed diff --git a/README.md b/README.md index 572bac60..f6f411c8 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,13 @@ Asynchronous publishing simply uploads the SBOM to Dependency-Track and the job ![job trend](docs/images/jenkins-job-trend.png) -![findings](docs/images/jenkins-job-findings.png) +![build summary](docs/images/jenkins-build-summary.png) +![findings](docs/images/jenkins-build-findings.png) ![policy violations](docs/images/jenkins-build-policy-violations.png) ## Global Configuration To setup, navigate to Jenkins > System Configuration and complete the Dependency-Track section. -![global configuration](docs/images/jenkins-global-odt.png) +![global configuration](docs/images/jenkins-global-settings.png) **Dependency-Track Backend URL**: URL to the Backend of your Dependency-Track instance. @@ -50,7 +51,8 @@ BOM_UPLOAD | :ballot_box_with_check: | needed for BOM upload VIEW_PORTFOLIO | :ballot_box_with_check: | needed to retrieve list of projects VULNERABILITY_ANALYSIS | :ballot_box_with_check: | needed to perform dependency analysis PROJECT_CREATION_UPLOAD | :grey_question: | needed to create non-existing projects during BOM upload -VIEW_VULNERABILITY | :grey_question: | needed in synchronous publishing mode to retrieve analysis results +VIEW_VULNERABILITY | :grey_question: | needed in synchronous publishing mode to retrieve results for vulnerabilities +VIEW_POLICY_VIOLATION | :grey_question: | needed in synchronous publishing mode to retrieve results for policy violations PORTFOLIO_MANAGEMENT | :grey_question: | needed for updating project properties such as tags ## Job Configuration @@ -66,7 +68,7 @@ Once configured with a valid URL and API key, simply configure a job to publish **Artifact:** Specifies the file to upload. Paths are relative from the Jenkins workspace. The use of environment variables in the form `${VARIABLE}` is supported here. -**Enable synchronous publishing mode**: Uploads a SBOM to Dependency-Track and waits for Dependency-Track to process and return results. The results returned are identical to the auditable findings but exclude findings that have previously been suppressed. Analysis decisions and vulnerability details are included in the response. Synchronous mode is possible with Dependency-Track v3.3.1 and higher. The provided API key requires the `VIEW_VULNERABILITY` permission to use this feature with Dependency-Track v4.4 and newer! +**Enable synchronous publishing mode**: Uploads a SBOM to Dependency-Track and waits for Dependency-Track to process and return results. The results returned are identical to the auditable findings but exclude findings that have previously been suppressed. Analysis decisions and vulnerability details are included in the response. Synchronous mode is possible with Dependency-Track v3.3.1 and higher. The provided API key requires the `VIEW_VULNERABILITY` permission to use this feature with Dependency-Track v4.4 and newer! If the provided API key has the permission `VIEW_POLICY_VIOLATION`, then the results of policy violations are returned as well. **Update project properties**: Allows updating of some project properties after uploading the BOM. The provided API key requires the `PORTFOLIO_MANAGEMENT` permission to use this feature! These properties are: - tags @@ -87,6 +89,10 @@ When synchronous mode is enabled, thresholds can be defined which can optionally **New Findings:** Sets the threshold for the number of new critical, high, medium, low or unassigned severity findings allowed. If the number of new findings equals or is greater than the previous builds finding for any one of the severities, the job status will be changed to UNSTABLE or FAILURE. The previous build is the one that is successful and has an analysis result of Dependency-Track, which does not necessarily have to be the immediately previous build. +### Policy Violations + +If synchronous mode is enabled, it is possible to set the job to the UNSTABLE or FAILURE state depending on the state of the policy violation. Policy violations are evaluated after the threshold values for vulnerability findings. + ## Examples ### Declarative Pipeline diff --git a/docs/images/jenkins-build-findings.png b/docs/images/jenkins-build-findings.png new file mode 100644 index 00000000..a154a7cb Binary files /dev/null and b/docs/images/jenkins-build-findings.png differ diff --git a/docs/images/jenkins-build-policy-violations.png b/docs/images/jenkins-build-policy-violations.png new file mode 100644 index 00000000..d82f41a2 Binary files /dev/null and b/docs/images/jenkins-build-policy-violations.png differ diff --git a/docs/images/jenkins-build-summary.png b/docs/images/jenkins-build-summary.png new file mode 100644 index 00000000..13c5f5ae Binary files /dev/null and b/docs/images/jenkins-build-summary.png differ diff --git a/docs/images/jenkins-global-odt.png b/docs/images/jenkins-global-odt.png deleted file mode 100644 index d4214118..00000000 Binary files a/docs/images/jenkins-global-odt.png and /dev/null differ diff --git a/docs/images/jenkins-global-settings.png b/docs/images/jenkins-global-settings.png new file mode 100644 index 00000000..57d7d13c Binary files /dev/null and b/docs/images/jenkins-global-settings.png differ diff --git a/docs/images/jenkins-job-findings.png b/docs/images/jenkins-job-findings.png deleted file mode 100644 index 4e7a4397..00000000 Binary files a/docs/images/jenkins-job-findings.png and /dev/null differ diff --git a/docs/images/jenkins-job-publish.png b/docs/images/jenkins-job-publish.png index 5e1ab384..fb608c0e 100644 Binary files a/docs/images/jenkins-job-publish.png and b/docs/images/jenkins-job-publish.png differ diff --git a/docs/images/jenkins-job-thresholds.png b/docs/images/jenkins-job-thresholds.png index 82ad341a..16fa7d74 100644 Binary files a/docs/images/jenkins-job-thresholds.png and b/docs/images/jenkins-job-thresholds.png differ diff --git a/docs/images/jenkins-job-trend-policy-violations.png b/docs/images/jenkins-job-trend-policy-violations.png new file mode 100644 index 00000000..163b5fd2 Binary files /dev/null and b/docs/images/jenkins-job-trend-policy-violations.png differ diff --git a/docs/images/jenkins-job-trend.png b/docs/images/jenkins-job-trend.png index 2aa7d3f8..f4332e83 100644 Binary files a/docs/images/jenkins-job-trend.png and b/docs/images/jenkins-job-trend.png differ diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java index 972d157a..6f964843 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ApiClient.java @@ -40,6 +40,7 @@ import org.jenkinsci.plugins.DependencyTrack.model.Project; import org.jenkinsci.plugins.DependencyTrack.model.Team; import org.jenkinsci.plugins.DependencyTrack.model.UploadResult; +import org.jenkinsci.plugins.DependencyTrack.model.Violation; import org.springframework.http.HttpStatus; import org.springframework.retry.RetryPolicy; import org.springframework.retry.backoff.UniformRandomBackOffPolicy; @@ -62,6 +63,7 @@ public class ApiClient { private static final String API_URL = "/api/v1"; static final String API_KEY_HEADER = "X-Api-Key"; static final String PROJECT_FINDINGS_URL = API_URL + "/finding/project"; + static final String PROJECT_VIOLATIONS_URL = API_URL + "/violation/project"; static final String BOM_URL = API_URL + "/bom"; static final String BOM_TOKEN_URL = BOM_URL + "/token"; static final String PROJECT_URL = API_URL + "/project"; @@ -266,6 +268,28 @@ public List getFindings(@NonNull final String projectUuid) throws ApiCl }); } + @NonNull + @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") + public List getViolations(@NonNull final String projectUuid) throws ApiClientException { + final var uri = UriComponentsBuilder.fromUriString(PROJECT_VIOLATIONS_URL).pathSegment("{uuid}").build(projectUuid); + final var request = createRequest(uri); + return executeWithRetry(() -> { + try (var response = httpClient.newCall(request).execute()) { + final var body = response.body().string(); + if (!response.isSuccessful()) { + final int status = response.code(); + logger.log(body); + throw new ApiClientException(Messages.ApiClient_Error_RetrieveViolations(status, HttpStatus.valueOf(status).getReasonPhrase())); + } + return ViolationParser.parse(body); + } catch (ApiClientException e) { + throw e; + } catch (IOException e) { + throw new ApiClientException(Messages.ApiClient_Error_Connection(StringUtils.EMPTY, StringUtils.EMPTY), e); + } + }); + } + @NonNull @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") public UploadResult upload(@Nullable final String projectId, @Nullable final String projectName, @Nullable final String projectVersion, @NonNull final FilePath artifact, diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ComponentParser.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ComponentParser.java new file mode 100644 index 00000000..cb09ace9 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ComponentParser.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 OWASP. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jenkinsci.plugins.DependencyTrack; + +import lombok.experimental.UtilityClass; +import net.sf.json.JSONObject; +import org.jenkinsci.plugins.DependencyTrack.model.Component; + +/** + * + * @author Ronny "Sephiroth" Perinke + */ +@UtilityClass +class ComponentParser extends ModelParser { + + Component parseComponent(JSONObject json) { + final String uuid = getKeyOrNull(json, "uuid"); + final String name = getKeyOrNull(json, "name"); + final String group = getKeyOrNull(json, "group"); + final String version = getKeyOrNull(json, "version"); + final String purl = getKeyOrNull(json, "purl"); + return new Component(uuid, name, group, version, purl); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher.java index c4e17747..c23bd14d 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher.java @@ -32,6 +32,7 @@ import hudson.tasks.Recorder; import hudson.util.Secret; import java.util.Optional; +import jenkins.model.RunAction2; import jenkins.tasks.SimpleBuildStep; import lombok.AccessLevel; import lombok.EqualsAndHashCode; @@ -43,11 +44,15 @@ import org.jenkinsci.plugins.DependencyTrack.model.SeverityDistribution; import org.jenkinsci.plugins.DependencyTrack.model.Thresholds; import org.jenkinsci.plugins.DependencyTrack.model.UploadResult; +import org.jenkinsci.plugins.DependencyTrack.model.Violation; +import org.jenkinsci.plugins.DependencyTrack.model.ViolationState; import org.jenkinsci.plugins.DependencyTrack.model.Vulnerability; import org.jenkinsci.plugins.plaincredentials.StringCredentials; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; +import static org.jenkinsci.plugins.DependencyTrack.model.Permissions.VIEW_POLICY_VIOLATION; + @Getter @Setter(onMethod_ = {@DataBoundSetter}) @EqualsAndHashCode(callSuper = true) @@ -235,6 +240,18 @@ public final class DependencyTrackPublisher extends Recorder implements SimpleBu */ private Integer failedNewUnassigned; + /** + * mark job as UNSTABLE if there is at least one policy violation of severity + * warning + */ + private boolean warnOnViolationWarn; + + /** + * mark job as FAILED if there is at least one policy violation of severity + * fail + */ + private boolean failOnViolationFail; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private transient ApiClientFactory clientFactory; @@ -324,9 +341,14 @@ public void perform(@NonNull final Run run, @NonNull final FilePath worksp final var thresholds = getThresholds(); if (synchronous && StringUtils.isNotBlank(uploadResult.getToken())) { - final var severityDistribution = publishAnalysisResult(logger, apiClient, uploadResult.getToken(), run, effectiveProjectName, effectiveProjectVersion); + final var resultActions = publishAnalysisResult(logger, apiClient, uploadResult.getToken(), run, effectiveProjectName, effectiveProjectVersion); if (thresholds.hasValues()) { - evaluateRiskGates(run, logger, severityDistribution, thresholds); + final var resultAction = resultActions.get(0).map(ResultAction.class::cast).get(); + evaluateRiskGates(run, logger, resultAction.getSeverityDistribution(), thresholds); + } + if (resultActions.get(1).isPresent()) { + final var violationsAction = resultActions.get(1).map(ViolationsRunAction.class::cast).get(); + evaluateViolations(run, logger, violationsAction.getViolations()); } } if (!synchronous && thresholds.hasValues()) { @@ -334,7 +356,7 @@ public void perform(@NonNull final Run run, @NonNull final FilePath worksp } } - private SeverityDistribution publishAnalysisResult(final ConsoleLogger logger, final ApiClient apiClient, final String token, final Run build, final String effectiveProjectName, final String effectiveProjectVersion) throws InterruptedException, ApiClientException, AbortException { + private List> publishAnalysisResult(final ConsoleLogger logger, final ApiClient apiClient, final String token, final Run build, final String effectiveProjectName, final String effectiveProjectVersion) throws InterruptedException, ApiClientException, AbortException { final long timeout = System.currentTimeMillis() + (60000L * getEffectivePollingTimeout()); final long interval = 1000L * getEffectivePollingInterval(); logger.log(Messages.Builder_Polling()); @@ -352,10 +374,24 @@ private SeverityDistribution publishAnalysisResult(final ConsoleLogger logger, f final List findings = apiClient.getFindings(effectiveProjectId); final SeverityDistribution severityDistribution = new SeverityDistribution(build.getNumber()); findings.stream().map(Finding::getVulnerability).map(Vulnerability::getSeverity).forEach(severityDistribution::add); - final ResultAction projectAction = new ResultAction(findings, severityDistribution); - projectAction.setDependencyTrackUrl(getEffectiveFrontendUrl()); - projectAction.setProjectId(effectiveProjectId); - build.addOrReplaceAction(projectAction); + final var findingsAction = new ResultAction(findings, severityDistribution); + findingsAction.setDependencyTrackUrl(getEffectiveFrontendUrl()); + findingsAction.setProjectId(effectiveProjectId); + build.addOrReplaceAction(findingsAction); + + final var team = apiClient.getTeamPermissions(); + ViolationsRunAction violationsAction = null; + // for compatibility reasons: the permission may not be present so we check if it is. otherwise an exception would be thrown. + if (team.getPermissions().contains(VIEW_POLICY_VIOLATION.toString())) { + logger.log(Messages.Builder_Violations_Processing()); + final var violations = apiClient.getViolations(effectiveProjectId); + violationsAction = new ViolationsRunAction(violations); + violationsAction.setDependencyTrackUrl(getEffectiveFrontendUrl()); + violationsAction.setProjectId(effectiveProjectId); + build.addOrReplaceAction(violationsAction); + } else { + logger.log(Messages.Builder_Violations_Skipped(VIEW_POLICY_VIOLATION, team.getName())); + } // update ResultLinkAction with one that surely contains a projectId final ResultLinkAction linkAction = new ResultLinkAction(getEffectiveFrontendUrl(), effectiveProjectId); @@ -363,7 +399,8 @@ private SeverityDistribution publishAnalysisResult(final ConsoleLogger logger, f linkAction.setProjectVersion(effectiveProjectVersion); build.addOrReplaceAction(linkAction); - return severityDistribution; + // replace with record when using Java 17 + return List.of(Optional.of(findingsAction), Optional.ofNullable(violationsAction)); } private void evaluateRiskGates(final Run build, final ConsoleLogger logger, final SeverityDistribution currentDistribution, final Thresholds thresholds) throws AbortException { @@ -390,6 +427,16 @@ private void evaluateRiskGates(final Run build, final ConsoleLogger logger } } + private void evaluateViolations(final Run build, final ConsoleLogger logger, final List violations) throws AbortException { + if (warnOnViolationWarn && violations.stream().anyMatch(violation -> violation.getState() == ViolationState.WARN)) { + logger.log(Messages.Builder_Violations_Exceed()); + build.setResult(Result.UNSTABLE); + } + if (failOnViolationFail && violations.stream().anyMatch(violation -> violation.getState() == ViolationState.FAIL)) { + throw new AbortException(Messages.Builder_Violations_Exceed()); + } + } + /** * * @return A Descriptor Implementation diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java index 57212e5c..7b84022b 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java @@ -296,8 +296,10 @@ private FormValidation checkTeamPermissions(final ApiClient apiClient, final Str } if (synchronous) { requiredPermissions.add(VIEW_VULNERABILITY.toString()); + requiredPermissions.add(VIEW_POLICY_VIOLATION.toString()); } else { optionalPermissions.add(VIEW_VULNERABILITY.toString()); + optionalPermissions.add(VIEW_POLICY_VIOLATION.toString()); } if (projectProperties) { requiredPermissions.add(PORTFOLIO_MANAGEMENT.toString()); diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/FindingParser.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/FindingParser.java index 23e154a7..b7b12c44 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/FindingParser.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/FindingParser.java @@ -24,7 +24,6 @@ import net.sf.json.JSONArray; import net.sf.json.JSONNull; import net.sf.json.JSONObject; -import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.DependencyTrack.model.Analysis; import org.jenkinsci.plugins.DependencyTrack.model.Component; import org.jenkinsci.plugins.DependencyTrack.model.Finding; @@ -32,7 +31,7 @@ import org.jenkinsci.plugins.DependencyTrack.model.Vulnerability; @UtilityClass -class FindingParser { +class FindingParser extends ModelParser { List parse(final String jsonResponse) { final JSONArray jsonArray = JSONArray.fromObject(jsonResponse); @@ -49,22 +48,13 @@ List parse(final String jsonResponse) { } private Finding parseFinding(JSONObject json) { - final Component component = parseComponent(json.getJSONObject("component")); + final Component component = ComponentParser.parseComponent(json.getJSONObject("component")); final Vulnerability vulnerability = parseVulnerability(json.getJSONObject("vulnerability")); final Analysis analysis = parseAnalysis(json.optJSONObject("analysis")); final String matrix = getKeyOrNull(json, "matrix"); return new Finding(component, vulnerability, analysis, matrix); } - private Component parseComponent(JSONObject json) { - final String uuid = getKeyOrNull(json, "uuid"); - final String name = getKeyOrNull(json, "name"); - final String group = getKeyOrNull(json, "group"); - final String version = getKeyOrNull(json, "version"); - final String purl = getKeyOrNull(json, "purl"); - return new Component(uuid, name, group, version, purl); - } - private Vulnerability parseVulnerability(JSONObject json) { final String uuid = getKeyOrNull(json, "uuid"); final String source = getKeyOrNull(json, "source"); @@ -73,7 +63,7 @@ private Vulnerability parseVulnerability(JSONObject json) { final String subtitle = getKeyOrNull(json, "subtitle"); final String description = getKeyOrNull(json, "description"); final String recommendation = getKeyOrNull(json, "recommendation"); - final Severity severity = json.has("severity") ? Severity.valueOf(json.getString("severity")) : null; + final Severity severity = getEnum(json, "severity", Severity.class); final Integer severityRank = json.optInt("severityRank"); final var cwe = Optional.ofNullable(json.optJSONArray("cwes")).map(a -> a.optJSONObject(0)).filter(Predicate.not(JSONNull.class::isInstance)); final Integer cweId = cwe.map(o -> o.optInt("cweId")).orElse(null); @@ -100,14 +90,4 @@ private List parseAliases(JSONObject json, String vulnId) { .collect(Collectors.toList()) : null; } - - private String getKeyOrNull(JSONObject json, String key) { - // key can be null. but it may also be JSONNull! - // optString and getString do not check if v is JSONNull. instead they return just v.toString() which will be "null"! - return Optional.ofNullable(json.opt(key)) - .filter(Predicate.not(JSONNull.class::isInstance)) - .map(Object::toString) - .map(StringUtils::trimToNull) - .orElse(null); - } } diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ModelParser.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ModelParser.java new file mode 100644 index 00000000..17d3b380 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ModelParser.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 OWASP. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jenkinsci.plugins.DependencyTrack; + +import java.util.Optional; +import java.util.function.Predicate; +import net.sf.json.JSONObject; +import net.sf.json.util.JSONUtils; +import org.apache.commons.lang.StringUtils; + +/** + * + * @author Ronny "Sephiroth" Perinke + */ +abstract class ModelParser { + + protected ModelParser() { + } + + protected static final String getKeyOrNull(final JSONObject json, final String key) { + // key can be null. but it may also be JSONNull! + // optString and getString do not check if v is JSONNull. instead they return just v.toString() which will be "null"! + return Optional.ofNullable(json.opt(key)) + .filter(Predicate.not(JSONUtils::isNull)) + .map(Object::toString) + .map(StringUtils::trimToNull) + .orElse(null); + } + + protected static final > T getEnum(final JSONObject json, final String key, final Class enumType) { + final var value = getKeyOrNull(json, key); + try { + return value != null ? Enum.valueOf(enumType, value) : null; + } catch (IllegalArgumentException ignore) { + return null; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectParser.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectParser.java index 6e87a70c..29973174 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectParser.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ProjectParser.java @@ -20,13 +20,12 @@ import java.util.stream.Collectors; import lombok.experimental.UtilityClass; import net.sf.json.JSONArray; -import net.sf.json.JSONNull; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.DependencyTrack.model.Project; @UtilityClass -class ProjectParser { +class ProjectParser extends ModelParser { Project parse(final JSONObject json) { final String lastInheritedRiskScoreStr = getKeyOrNull(json, "lastInheritedRiskScore"); @@ -43,20 +42,10 @@ Project parse(final JSONObject json) { .active(activeStr != null ? Boolean.valueOf(activeStr) : null) .swidTagId(getKeyOrNull(json, "swidTagId")) .group(getKeyOrNull(json, "group")) - .parent(json.has("parent") ? ProjectParser.parse(json.getJSONObject("parent")) : null) + .parent(json.has("parent") ? parse(json.getJSONObject("parent")) : null) .build(); } - private String getKeyOrNull(JSONObject json, String key) { - // key can be null. but it may also be JSONNull! - // optString and getString do not check if v is JSONNull. instead they return just v.toString() which will be "null"! - Object v = json.opt(key); - if (v instanceof JSONNull) { - v = null; - } - return v == null ? null : StringUtils.trimToNull(v.toString()); - } - private LocalDateTime parseDateTime(String dateTime) { if (dateTime == null) { return null; diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ViolationParser.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ViolationParser.java new file mode 100644 index 00000000..ae7bcf58 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ViolationParser.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 OWASP. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jenkinsci.plugins.DependencyTrack; + +import java.util.List; +import java.util.stream.Collectors; +import lombok.experimental.UtilityClass; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; +import org.jenkinsci.plugins.DependencyTrack.model.Violation; +import org.jenkinsci.plugins.DependencyTrack.model.ViolationState; +import org.jenkinsci.plugins.DependencyTrack.model.ViolationType; + +@UtilityClass +class ViolationParser extends ModelParser { + + List parse(final String jsonResponse) { + final JSONArray jsonArray = JSONArray.fromObject(jsonResponse); + return jsonArray.stream() + .map(JSONObject.class::cast) + .map(ViolationParser::parseViolation) + .collect(Collectors.toList()); + } + + private Violation parseViolation(JSONObject json) { + final var uuid = getKeyOrNull(json, "uuid"); + final var type = getEnum(json, "type", ViolationType.class); + final var policy = json.getJSONObject("policyCondition").getJSONObject("policy"); + final var state = getEnum(policy, "violationState", ViolationState.class); + final var policyName = getKeyOrNull(policy, "name"); + final var component = ComponentParser.parseComponent(json.getJSONObject("component")); + return new Violation(uuid, type, state, policyName, component); + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ViolationsJobAction.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ViolationsJobAction.java new file mode 100644 index 00000000..8b2c9d6e --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ViolationsJobAction.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024 OWASP. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jenkinsci.plugins.DependencyTrack; + +import hudson.model.InvisibleAction; +import hudson.model.Job; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; +import org.jenkinsci.plugins.DependencyTrack.model.ViolationState; +import org.kohsuke.stapler.WebApp; +import org.kohsuke.stapler.bind.JavaScriptMethod; + +/** + * + * @author Ronny "Sephiroth" Perinke + */ +@RequiredArgsConstructor +public class ViolationsJobAction extends InvisibleAction { + + @Getter + @NonNull + private final Job project; + + @Override + public String getUrlName() { + return "dtrackTrend"; + } + + /** + * Returns whether the policy violations trend chart is visible or not. + * + * @return {@code true} if the trend is visible, false otherwise + */ + public boolean isTrendVisible() { + return project.getBuilds().stream() + .map(run -> run.getAction(ViolationsRunAction.class)) + .anyMatch(Objects::nonNull); + } + + /** + * Returns the UI model for an ECharts line chart that shows the violations + * stacked by state. + * + * @return the UI model as JSON + */ + @JavaScriptMethod + public JSONArray getViolationsTrend() { + project.checkPermission(hudson.model.Item.READ); + final List distributions = project.getBuilds().stream() + .sorted(Comparator.naturalOrder()) + .map(run -> run.getAction(ViolationsRunAction.class)).filter(Objects::nonNull) + .map(result -> { + final var violations = result.getViolations() + .stream() + .collect(Collectors.toMap(violation -> violation.getState().name().toLowerCase(), i -> 1, (a, b) -> a + b)); + final var item = new JSONObject(); + item.element("buildNumber", result.getRun().getNumber()); + item.putAll(violations); + // ensure all keys exist + item.putIfAbsent(ViolationState.INFO.name().toLowerCase(), 0); + item.putIfAbsent(ViolationState.WARN.name().toLowerCase(), 0); + item.putIfAbsent(ViolationState.FAIL.name().toLowerCase(), 0); + return item; + }) + .collect(Collectors.toList()); + return JSONArray.fromObject(distributions); + } + + public String getBindUrl() { + return WebApp.getCurrent().boundObjectTable.bind(this).getURL(); + } + + public String getCrumb() { + return WebApp.getCurrent().getCrumbIssuer().issueCrumb(); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/ViolationsRunAction.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ViolationsRunAction.java new file mode 100644 index 00000000..36a439ab --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/ViolationsRunAction.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 OWASP. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jenkinsci.plugins.DependencyTrack; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Plugin; +import hudson.PluginWrapper; +import hudson.model.Action; +import hudson.model.Run; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import jenkins.model.Jenkins; +import jenkins.model.RunAction2; +import jenkins.tasks.SimpleBuildStep; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import net.sf.json.JSONArray; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.DependencyTrack.model.Violation; +import org.kohsuke.stapler.WebApp; +import org.kohsuke.stapler.bind.JavaScriptMethod; + +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +public class ViolationsRunAction implements RunAction2, SimpleBuildStep.LastBuildAction, Serializable { + + private static final long serialVersionUID = 8223620580665511318L; + + private transient Run run; // transient: see RunAction2, and JENKINS-45892 + private final List violations; + + /** + * the URL of the Dependency-Track Server to which these results are + * belonging to + */ + @Setter + private String dependencyTrackUrl; + + /** + * the ID of the project to which these results are belonging to + */ + @Setter + private String projectId; + + @Override + public String getIconFileName() { + return "/plugin/dependency-track/icons/dt-logo-symbol.svg"; + } + + @Override + public String getDisplayName() { + return Messages.Result_DT_ReportViolations(); + } + + @Override + public String getUrlName() { + return "dependency-track-violations"; + } + + @Override + public void onAttached(Run run) { + this.run = run; + } + + @Override + public void onLoad(Run run) { + this.run = run; + } + + @Override + public Collection getProjectActions() { + return Set.of(new ViolationsJobAction(run.getParent())); + } + + @NonNull + public String getVersionHash() { + return DigestUtils.sha256Hex( + Optional.ofNullable(Jenkins.get().getPlugin("dependency-track")) + .map(Plugin::getWrapper) + .map(PluginWrapper::getVersion) + .orElse(StringUtils.EMPTY) + ); + } + + public boolean hasViolations() { + return violations != null && !violations.isEmpty(); + } + + /** + * Returns the UI model for an ECharts line chart that shows the violations. + * + * @return the UI model as JSON + */ + @JavaScriptMethod + public JSONArray getViolationsJson() { + run.checkPermission(hudson.model.Item.READ); + return JSONArray.fromObject(violations); + } + + public String getBindUrl() { + return WebApp.getCurrent().boundObjectTable.bind(this).getURL(); + } + + public String getCrumb() { + return WebApp.getCurrent().getCrumbIssuer().issueCrumb(); + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Permissions.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Permissions.java index 655610c7..bd4ffb13 100644 --- a/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Permissions.java +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Permissions.java @@ -20,5 +20,5 @@ * @author Ronny "Sephiroth" Perinke */ public enum Permissions { - BOM_UPLOAD, VIEW_PORTFOLIO, VULNERABILITY_ANALYSIS, PROJECT_CREATION_UPLOAD, PORTFOLIO_MANAGEMENT, VIEW_VULNERABILITY + BOM_UPLOAD, VIEW_PORTFOLIO, VULNERABILITY_ANALYSIS, PROJECT_CREATION_UPLOAD, PORTFOLIO_MANAGEMENT, VIEW_VULNERABILITY, VIEW_POLICY_VIOLATION } diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Violation.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Violation.java new file mode 100644 index 00000000..71067b96 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/Violation.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 OWASP. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jenkinsci.plugins.DependencyTrack.model; + +import java.io.Serializable; +import lombok.EqualsAndHashCode; +import lombok.Value; + +/** + * + * @author Ronny "Sephiroth" Perinke + */ +@Value +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class Violation implements Serializable { + + private static final long serialVersionUID = -2591514706071774767L; + + @EqualsAndHashCode.Include + private final String uuid; + private final ViolationType type; + private final ViolationState state; + private final String policyName; + private final Component component; + + public int getStateRank() { + return state.ordinal(); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/ViolationState.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/ViolationState.java new file mode 100644 index 00000000..efbeeeb9 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/ViolationState.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 OWASP. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jenkinsci.plugins.DependencyTrack.model; + +/** + * + * @author Ronny "Sephiroth" Perinke + */ +public enum ViolationState { + FAIL, + WARN, + INFO +} diff --git a/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/ViolationType.java b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/ViolationType.java new file mode 100644 index 00000000..a86574e6 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/DependencyTrack/model/ViolationType.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 OWASP. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jenkinsci.plugins.DependencyTrack.model; + +/** + * + * @author Ronny "Sephiroth" Perinke + */ +public enum ViolationType { + LICENSE, + SECURITY, + OPERATIONAL +} diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config.jelly b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config.jelly index 437e4470..00f738ed 100644 --- a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config.jelly @@ -67,6 +67,12 @@ limitations under the License. + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config.properties b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config.properties index 314d7880..45a2829f 100644 --- a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config.properties +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config.properties @@ -40,3 +40,5 @@ Low=Low Finding Unassigned=Unassigned Finding Unstable=Unstable Failure=Failure +warnOnViolationWarn=Mark build as unstable on policy violations of type WARN +failOnViolationFail=Fail build on policy violations of type FAIL diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config_de.properties b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config_de.properties index 6870c6f9..934fd8e3 100644 --- a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config_de.properties +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/config_de.properties @@ -40,3 +40,5 @@ Low=Niedriger Befund Unassigned=Unkategorisierter Befund Unstable=Instabil Failure=Fehlgeschlagen +warnOnViolationWarn=Lauf als instabil markieren, wenn es Richtlinienverst\u00f6\u00dfe mit Schweregrad Warnung gibt +failOnViolationFail=Lauf fehlschlagen lassen, wenn es Richtlinienverst\u00f6\u00dfe mit Schweregrad Fehler gibt diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-failOnViolationFail.html b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-failOnViolationFail.html new file mode 100644 index 00000000..427eafd0 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-failOnViolationFail.html @@ -0,0 +1,4 @@ +
+

Marks the current build as unstable if there is at least one policy violation of severity failure.

+

This setting applies only to synchronous publishing mode!

+
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-failOnViolationFail_de.html b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-failOnViolationFail_de.html new file mode 100644 index 00000000..dc5114a8 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-failOnViolationFail_de.html @@ -0,0 +1,4 @@ +
+

Den Lauf fehlschlagen lassen, wenn es Richtlinienverstöße mit Schweregrad Fehler gibt.

+

Diese Einstellung gilt nur für den synchronen Veröffentlichungsmodus!

+
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-warnOnViolationWarn.html b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-warnOnViolationWarn.html new file mode 100644 index 00000000..ae78d610 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-warnOnViolationWarn.html @@ -0,0 +1,4 @@ +
+

Marks the current build as unstable if there is at least one policy violation of severity warning.

+

This setting applies only to synchronous publishing mode!

+
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-warnOnViolationWarn_de.html b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-warnOnViolationWarn_de.html new file mode 100644 index 00000000..efad3da3 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/DependencyTrackPublisher/help-warnOnViolationWarn_de.html @@ -0,0 +1,4 @@ +
+

Den Lauf fehlschlagen lassen, wenn es Richtlinienverstöße mit Schweregrad Warnung gibt.

+

Diese Einstellung gilt nur für den synchronen Veröffentlichungsmodus!

+
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/JobAction/floatingBox.jelly b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/JobAction/floatingBox.jelly index 524c01f8..f3d773c7 100644 --- a/src/main/resources/org/jenkinsci/plugins/DependencyTrack/JobAction/floatingBox.jelly +++ b/src/main/resources/org/jenkinsci/plugins/DependencyTrack/JobAction/floatingBox.jelly @@ -2,7 +2,7 @@ -