diff --git a/docs/_docs/datasources/composer.md b/docs/_docs/datasources/composer.md new file mode 100644 index 0000000000..eb9817a237 --- /dev/null +++ b/docs/_docs/datasources/composer.md @@ -0,0 +1,16 @@ +--- +title: Composer Advisories +category: Datasources +chapter: 4 +order: 11 +--- + +[Composer Advisories](https://blog.packagist.com/discover-security-advisories-with-composers-audit-command/) (PKSA) is a database of CVEs and other vulnerabilities affecting the Composer packages. Advisories may or may not be documented in the [National Vulnerability Database]({{ site.baseurl }}{% link _docs/datasources/nvd.md %}). + +Dependency-Track integrates with Composer by mirroring advisories via each repositories [Packagist API](https://packagist.org/apidoc). +The mirroring (and alias synchronization) can be enabled/disabled [per repository]({{ site.baseurl }}{% link _docs/datasources/repositories.md %}). +The mirror is refreshed daily, or upon restart of the Dependency-Track instance. + +![Composer Advisories Configuration](../../images/screenshots/composer-repository-configuration.png) + + diff --git a/docs/_docs/datasources/repositories.md b/docs/_docs/datasources/repositories.md index f94c3d607c..0e03f22912 100644 --- a/docs/_docs/datasources/repositories.md +++ b/docs/_docs/datasources/repositories.md @@ -65,4 +65,4 @@ for information on Package URL and the various ways it is used throughout Depend For GitHub repositories (`github.com` per default), the username should be the GitHub account's username, and the password should be a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) -(PAT) with public access (no additional scopes). +(PAT) with public access (no additional scopes). \ No newline at end of file diff --git a/docs/images/screenshots/composer-repository-configuration.png b/docs/images/screenshots/composer-repository-configuration.png new file mode 100755 index 0000000000..2c065ada94 Binary files /dev/null and b/docs/images/screenshots/composer-repository-configuration.png differ diff --git a/src/main/java/org/dependencytrack/event/ComposerAdvisoryMirrorEvent.java b/src/main/java/org/dependencytrack/event/ComposerAdvisoryMirrorEvent.java new file mode 100644 index 0000000000..c5c5ccb157 --- /dev/null +++ b/src/main/java/org/dependencytrack/event/ComposerAdvisoryMirrorEvent.java @@ -0,0 +1,40 @@ +/* + * This file is part of Dependency-Track. + * + * 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 + * + * http://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. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.event; + +import java.util.UUID; + +import alpine.event.framework.SingletonCapableEvent; + +/** + * Defines an event used to start a mirror of the NVD. + * + * @author Valentijn Scholten + * @since 4.13.0 + */ +public class ComposerAdvisoryMirrorEvent extends SingletonCapableEvent { + + private static final UUID CHAIN_IDENTIFIER = UUID.fromString("62710465-3117-4cc1-ad9a-d1dce721aa86"); + + public ComposerAdvisoryMirrorEvent() { + setChainIdentifier(CHAIN_IDENTIFIER); + setSingleton(true); + } + +} diff --git a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index c4784dbea6..eed71d51d6 100644 --- a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -50,6 +50,7 @@ import org.dependencytrack.tasks.metrics.PortfolioMetricsUpdateTask; import org.dependencytrack.tasks.metrics.ProjectMetricsUpdateTask; import org.dependencytrack.tasks.metrics.VulnerabilityMetricsUpdateTask; +import org.dependencytrack.tasks.repositories.ComposerAdvisoryMirrorTask; import org.dependencytrack.tasks.repositories.RepositoryMetaAnalyzerTask; import org.dependencytrack.tasks.scanners.InternalAnalysisTask; import org.dependencytrack.tasks.scanners.OssIndexAnalysisTask; @@ -99,6 +100,7 @@ public void contextInitialized(final ServletContextEvent event) { EVENT_SERVICE.subscribe(GitHubAdvisoryMirrorEvent.class, GitHubAdvisoryMirrorTask.class); EVENT_SERVICE.subscribe(OsvMirrorEvent.class, OsvDownloadTask.class); EVENT_SERVICE.subscribe(VulnDbSyncEvent.class, VulnDbSyncTask.class); + EVENT_SERVICE.subscribe(ComposerAdvisoryMirrorEvent.class, ComposerAdvisoryMirrorTask.class); EVENT_SERVICE.subscribe(VulnDbAnalysisEvent.class, VulnDbAnalysisTask.class); EVENT_SERVICE.subscribe(VulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class); EVENT_SERVICE.subscribe(PortfolioVulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class); @@ -141,6 +143,7 @@ public void contextDestroyed(final ServletContextEvent event) { EVENT_SERVICE.unsubscribe(InternalAnalysisTask.class); EVENT_SERVICE.unsubscribe(OssIndexAnalysisTask.class); EVENT_SERVICE.unsubscribe(GitHubAdvisoryMirrorTask.class); + EVENT_SERVICE.unsubscribe(ComposerAdvisoryMirrorTask.class); EVENT_SERVICE.unsubscribe(OsvDownloadTask.class); EVENT_SERVICE.unsubscribe(VulnDbSyncTask.class); EVENT_SERVICE.unsubscribe(VulnDbAnalysisTask.class); diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index c01d0da9e6..333f81b959 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -102,10 +102,11 @@ public enum ConfigPropertyConstants { NOTIFICATION_TEMPLATE_BASE_DIR("notification", "template.baseDir", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_BASE_DIRECTORY", System.getProperty("user.home")), PropertyType.STRING, "The base directory to use when searching for notification templates"), NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED("notification", "template.default.override.enabled", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_ENABLED", "false"), PropertyType.BOOLEAN, "Flag to enable/disable override of default notification templates"), TASK_SCHEDULER_LDAP_SYNC_CADENCE("task-scheduler", "ldap.sync.cadence", "6", PropertyType.INTEGER, "Sync cadence (in hours) for LDAP"), - TASK_SCHEDULER_GHSA_MIRROR_CADENCE("task-scheduler", "ghsa.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for Github Security Advisories"), + TASK_SCHEDULER_GHSA_MIRROR_CADENCE("task-scheduler", "ghsa.mirror.cadence", "1", PropertyType.INTEGER, "Mirror cadence (in hours) for Github Security Advisories"), TASK_SCHEDULER_OSV_MIRROR_CADENCE("task-scheduler", "osv.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for OSV database"), TASK_SCHEDULER_NIST_MIRROR_CADENCE("task-scheduler", "nist.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for NVD database"), TASK_SCHEDULER_VULNDB_MIRROR_CADENCE("task-scheduler", "vulndb.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for VulnDB database"), + TASK_SCHEDULER_COMPOSER_MIRROR_CADENCE("task-scheduler", "composer.mirror.cadencee", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for Vulnerability mirroring of Composer repositories"), TASK_SCHEDULER_PORTFOLIO_METRICS_UPDATE_CADENCE("task-scheduler", "portfolio.metrics.update.cadence", "1", PropertyType.INTEGER, "Update cadence (in hours) for portfolio metrics"), TASK_SCHEDULER_VULNERABILITY_METRICS_UPDATE_CADENCE("task-scheduler", "vulnerability.metrics.update.cadence", "1", PropertyType.INTEGER, "Update cadence (in hours) for vulnerability metrics"), TASK_SCHEDULER_PORTFOLIO_VULNERABILITY_ANALYSIS_CADENCE("task-scheduler", "portfolio.vulnerability.analysis.cadence", "24", PropertyType.INTEGER, "Launch cadence (in hours) for portfolio vulnerability analysis"), diff --git a/src/main/java/org/dependencytrack/model/Finding.java b/src/main/java/org/dependencytrack/model/Finding.java index 13867227b2..c7afa7bb67 100644 --- a/src/main/java/org/dependencytrack/model/Finding.java +++ b/src/main/java/org/dependencytrack/model/Finding.java @@ -301,6 +301,12 @@ public void addVulnerabilityAliases(List aliases) { if (alias.getVulnDbId() != null && !alias.getVulnDbId().isBlank()) { map.put("vulnDbId", alias.getVulnDbId()); } + if (alias.getDrupalId() != null && !alias.getDrupalId().isBlank()) { + map.put("drupalId", alias.getDrupalId()); + } + if (alias.getComposerId() != null && !alias.getComposerId().isBlank()) { + map.put("composerId", alias.getComposerId()); + } uniqueAliases.add(map); } vulnerability.put("aliases",uniqueAliases); diff --git a/src/main/java/org/dependencytrack/model/Repository.java b/src/main/java/org/dependencytrack/model/Repository.java index 688045718f..a26a6f20d1 100644 --- a/src/main/java/org/dependencytrack/model/Repository.java +++ b/src/main/java/org/dependencytrack/model/Repository.java @@ -65,6 +65,11 @@ public class Repository implements Serializable { @JsonDeserialize(using = TrimmedStringDeserializer.class) private String identifier; + @Persistent + @Column(name = "DESCRIPTION") + @JsonDeserialize(using = TrimmedStringDeserializer.class) + private String description; + @Persistent @Column(name = "URL") @NotBlank @@ -100,6 +105,11 @@ public class Repository implements Serializable { @Column(name = "PASSWORD") private String password; + @Persistent + @Column(name = "CONFIG", jdbcType = "CLOB") + @JsonDeserialize(using = TrimmedStringDeserializer.class) + private String config; + @Persistent(customValueStrategy = "uuid") @Index(name = "REPOSITORY_UUID_IDX") // Cannot be @Unique. Microsoft SQL Server throws an exception @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "true") @@ -131,6 +141,14 @@ public void setIdentifier(String identifier) { this.identifier = identifier; } + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + public String getUrl() { return url; } @@ -189,6 +207,14 @@ public void setPassword(String password) { this.password = password; } + public String getConfig() { + return config; + } + + public void setConfig(String config) { + this.config = config; + } + public UUID getUuid() { return uuid; } diff --git a/src/main/java/org/dependencytrack/model/Vulnerability.java b/src/main/java/org/dependencytrack/model/Vulnerability.java index 13f52cc395..821b4194b4 100644 --- a/src/main/java/org/dependencytrack/model/Vulnerability.java +++ b/src/main/java/org/dependencytrack/model/Vulnerability.java @@ -28,6 +28,8 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; + +import org.apache.commons.lang3.StringUtils; import org.dependencytrack.parser.common.resolver.CweResolver; import org.dependencytrack.persistence.CollectionIntegerConverter; import org.dependencytrack.resources.v1.serializers.CweDeserializer; @@ -111,6 +113,8 @@ public enum Source { OSV, // Google OSV Advisories SNYK, // Snyk TRIVY, // Trivy + COMPOSER, // Composer (Packagist) + DRUPAL, // Drupal UNKNOWN; // Unknown vulnerability sources public static boolean isKnownSource(String source) { @@ -118,12 +122,20 @@ public static boolean isKnownSource(String source) { } public static Source resolve(String id) { + if (StringUtils.isBlank(id)) { + return UNKNOWN; + } + if (id.startsWith("CVE-")){ return NVD; } else if (id.startsWith("GHSA-")){ return GITHUB; } else if (id.startsWith("OSV-")){ return OSV; + } else if (id.startsWith("SA-CORE-") || id.startsWith("SA-CONTRIB")){ + return DRUPAL; + } else if (id.startsWith("PKSA-")){ + return COMPOSER; } else if (id.startsWith("SNYK-")){ return SNYK; } diff --git a/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java b/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java index f734edd2f8..6260cdc309 100644 --- a/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java +++ b/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java @@ -34,6 +34,9 @@ import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; import javax.jdo.annotations.Unique; + +import org.apache.commons.lang3.StringUtils; + import java.io.Serializable; import java.util.Arrays; import java.util.Map; @@ -111,6 +114,20 @@ public class VulnerabilityAlias implements Serializable { @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The vulnDbId field may only contain printable characters") private String vulnDbId; + @Persistent + @Column(name = "DRUPAL_ID") + @Index(name = "VULNERABILITYALIAS_DRUPAL_ID_IDX") + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The drupalId field may only contain printable characters") + private String drupalId; + + @Persistent + @Column(name = "COMPOSER_ID") + @Index(name = "VULNERABILITYALIAS_COMPOSER_ID_IDX") + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The composerId field may only contain printable characters") + private String composerId; + @Persistent(customValueStrategy = "uuid") @Unique(name = "VULNERABILITYALIAS_UUID_IDX") @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "false") @@ -185,6 +202,22 @@ public void setVulnDbId(String vulnDbId) { this.vulnDbId = vulnDbId; } + public String getDrupalId() { + return drupalId; + } + + public void setDrupalId(String drupalId) { + this.drupalId = drupalId; + } + + public String getComposerId() { + return composerId; + } + + public void setComposerId(String composerId) { + this.composerId = composerId; + } + public UUID getUuid() { return uuid; } @@ -202,6 +235,8 @@ private String getBySource(final Vulnerability.Source source) { case OSV -> getOsvId(); case SNYK -> getSnykId(); case VULNDB -> getVulnDbId(); + case DRUPAL -> getDrupalId(); + case COMPOSER -> getComposerId(); default -> null; }; } @@ -224,6 +259,8 @@ public void copyFieldsFrom(VulnerabilityAlias other) { gsdId = firstNonNull(other.gsdId, gsdId); vulnDbId = firstNonNull(other.vulnDbId, vulnDbId); internalId = firstNonNull(other.internalId, internalId); + drupalId = firstNonNull(other.drupalId, drupalId); + composerId = firstNonNull(other.composerId, composerId); } private static String firstNonNull(String first, String second) { @@ -263,10 +300,71 @@ public int computeMatches(final VulnerabilityAlias other) { if (this.getVulnDbId() != null && this.getVulnDbId().equals(other.getVulnDbId())) { matches++; } + if (this.getDrupalId() != null && this.getDrupalId().equals(other.getDrupalId())) { + matches++; + } + if (this.getComposerId() != null && this.getComposerId().equals(other.getComposerId())) { + matches++; + } return matches; } + /** + * Compute how many identifiers are set for this alias record + * + * @return Number of identifiers set + */ + public int countIdentifiers() { + var count = 0; + count += this.getCveId() != null ? 1 : 0; + count += this.getGhsaId() != null ? 1 : 0; + count += this.getGsdId() != null ? 1 : 0; + count += this.getOsvId() != null ? 1 : 0; + count += this.getSnykId() != null ? 1 : 0; + count += this.getSonatypeId() != null ? 1 : 0; + count += this.getVulnDbId() != null ? 1 : 0; + count += this.getDrupalId() != null ? 1 : 0; + count += this.getComposerId() != null ? 1 : 0; + return count; + } + + public boolean setAliasFromVulnId(String vulnId) { + if (StringUtils.isBlank(vulnId)) return false; + + if (vulnId.startsWith("GHSA") && this.getGhsaId() == null) { + this.setGhsaId(vulnId); + return true; + } + if (vulnId.startsWith("CVE") && this.getCveId() == null) { + this.setCveId(vulnId); + return true; + } + if ((vulnId.startsWith("SA-CORE") || vulnId.startsWith("SA-CONTRIB")) && this.getDrupalId() == null) { + this.setDrupalId(vulnId); + return true; + } + if (vulnId.startsWith("OSSINDEX") && this.getSonatypeId() == null) { + this.setSonatypeId(vulnId); + return true; + } + if (vulnId.startsWith("SNYK") && this.getSnykId() == null) { + this.setSnykId(vulnId); + return true; + } + if (vulnId.startsWith("GSD") && this.getGsdId() == null) { + this.setGsdId(vulnId); + return true; + } + if (vulnId.startsWith("PKSA") && this.getComposerId() == null) { + this.setComposerId(vulnId); + return true; + } + + return false; + } + + @Override public String toString() { return "VulnerabilityAlias{" + @@ -279,8 +377,9 @@ public String toString() { ", snykId='" + snykId + '\'' + ", gsdId='" + gsdId + '\'' + ", vulnDbId='" + vulnDbId + '\'' + + ", composerId='" + composerId + '\'' + + ", drupalId='" + drupalId + '\'' + ", uuid=" + uuid + '}'; } - } diff --git a/src/main/java/org/dependencytrack/notification/NotificationConstants.java b/src/main/java/org/dependencytrack/notification/NotificationConstants.java index 83e78c5339..74cb6480d6 100644 --- a/src/main/java/org/dependencytrack/notification/NotificationConstants.java +++ b/src/main/java/org/dependencytrack/notification/NotificationConstants.java @@ -24,6 +24,7 @@ public static class Title { public static final String NOTIFICATION_TEST = "Notification Test"; public static final String NVD_MIRROR = "NVD Mirroring"; public static final String GITHUB_ADVISORY_MIRROR = "GitHub Advisory Mirroring"; + public static final String COMPOSER_ADVISORY_MIRROR = "Composer Advisory Mirroring"; public static final String EPSS_MIRROR = "EPSS Mirroring"; public static final String NPM_ADVISORY_MIRROR = "NPM Advisory Mirroring"; public static final String VULNDB_MIRROR = "VulnDB Mirroring"; diff --git a/src/main/java/org/dependencytrack/parser/composer/ComposerAdvisoryParser.java b/src/main/java/org/dependencytrack/parser/composer/ComposerAdvisoryParser.java new file mode 100644 index 0000000000..a6c164abb6 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/composer/ComposerAdvisoryParser.java @@ -0,0 +1,101 @@ +/* + * This file is part of Dependency-Track. + * + * 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 + * + * http://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. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.parser.composer; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; + +import org.dependencytrack.parser.composer.model.ComposerAdvisory; +import org.json.JSONArray; +import org.json.JSONObject; + +import alpine.common.logging.Logger; + + +public class ComposerAdvisoryParser { + + private static final Logger LOGGER = Logger.getLogger(ComposerAdvisoryParser.class); + + public static List parseAdvisoryFeed(final JSONObject object) { + final List result = new ArrayList<>(); + final JSONObject advisories = object.optJSONObject("advisories"); + if (advisories != null) { + advisories.names().forEach(packageName -> { + final JSONArray advisory = advisories.optJSONArray((String)packageName); + if (advisory != null) { + for (int i = 0; i < advisory.length(); i++) { + final ComposerAdvisory composerAdvisory = parseAdvisory(advisory.getJSONObject(i)); + if (composerAdvisory != null) { + result.add(composerAdvisory); + } + } + + } + }); + } + return result; + } + + public static ComposerAdvisory parseAdvisory(final JSONObject object) { + final ComposerAdvisory vulnerability = new ComposerAdvisory(); + + //There's no status field in the advisory object, so we cannot check if the advisory has been withdrawn + vulnerability.setAdvisoryId(object.getString("advisoryId")); + vulnerability.setPackageName(object.optString("packageName", null)); + vulnerability.setRemoteId(object.optString("remoteId", null)); + vulnerability.setTitle(object.optString("title", null)); + vulnerability.setLink(object.optString("link", null)); + vulnerability.setCve(object.optString("cve", null)); + vulnerability.setAffectedVersionsCve(object.optString("affectedVersions", null)); + vulnerability.setSource(object.optString("source", null)); + + String reportedAtStr = object.optString("reportedAt", null); + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + if (reportedAtStr != null) { + LocalDateTime reportedAt = LocalDateTime.parse(reportedAtStr, formatter); + vulnerability.setReportedAt(reportedAt); + } + } catch (DateTimeParseException e) { + LOGGER.debug("Unabled to parse as LocalDateTime: " + reportedAtStr); + } + + vulnerability.setComposerRepository(object.optString("composerRepository", null)); + vulnerability.setSeverity(object.optString("severity", null)); + + //Some repositories like drupal use something weird as name that might not be unique + final JSONArray identifiers = object.optJSONArray("sources"); + if (identifiers != null) { + for (int i = 0; i < identifiers.length(); i++) { + final JSONObject identifier = identifiers.getJSONObject(i); + final String name = identifier.optString("name", null); + final String remoteId = identifier.optString("remoteId", null); + if (name != null && remoteId != null) { + vulnerability.addSource(name, remoteId); + } + } + } + + return vulnerability; + } + +} diff --git a/src/main/java/org/dependencytrack/parser/composer/model/ComposerAdvisory.java b/src/main/java/org/dependencytrack/parser/composer/model/ComposerAdvisory.java new file mode 100644 index 0000000000..3a05cb6331 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/composer/model/ComposerAdvisory.java @@ -0,0 +1,146 @@ +/* + * This file is part of Dependency-Track. + * + * 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 + * + * http://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. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.parser.composer.model; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +public class ComposerAdvisory { + + private String advisoryId; + private String packageName; + private String remoteId; + private String title; + private String link; + private String cve; + private String affectedVersions; + private String source; + private LocalDateTime reportedAt; + private String composerRepository; + private String severity; + private Map sources; + + public String getAdvisoryId() { + return advisoryId; + } + + public void setAdvisoryId(String advisoryId) { + this.advisoryId = advisoryId; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getRemoteId() { + return remoteId; + } + + public void setRemoteId(String remoteId) { + this.remoteId = remoteId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + public String getCve() { + return cve; + } + + public void setCve(String cve) { + this.cve = cve; + } + + public String getAffectedVersions() { + return affectedVersions; + } + + public void setAffectedVersionsCve(String affectedVersions) { + this.affectedVersions = affectedVersions; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public LocalDateTime getReportedAt() { + return reportedAt; + } + + public void setReportedAt(LocalDateTime reportedAt) { + this.reportedAt = reportedAt; + } + + public String getComposerRepository() { + return composerRepository; + } + + public void setComposerRepository(String composerRepository) { + this.composerRepository = composerRepository; + } + + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + public Map getSources() { + if (sources == null) { + return new HashMap<>(); + } + return sources; + } + + public void addSource(String name, String remoteId) { + if (this.sources == null) { + this.sources = new HashMap<>(); + } + this.sources.put(name.toLowerCase(), remoteId); + } + + public String getPackageEcosystem() { + return "composer"; + } + +} diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 1cc6729b57..b6e5b0bc2c 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -214,24 +214,25 @@ private List getBadgesPermissions(final List fullList) { public void loadDefaultRepositories() { try (QueryManager qm = new QueryManager()) { LOGGER.info("Synchronizing default repositories to datastore"); - qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false, false, null, null); - qm.createRepository(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.HEX, "hex.pm", "https://hex.pm/", true, false, false, null, null); - qm.createRepository(RepositoryType.HACKAGE, "hackage.haskell.org", "https://hackage.haskell.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", true, false, false, null, null); - qm.createRepository(RepositoryType.NIXPKGS, "nixpkgs-unstable", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", true, false, false, null, null); - qm.createRepository(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", true, false, false, null, null); - qm.createRepository(RepositoryType.CARGO, "crates.io", "https://crates.io", true, false, false, null, null); - qm.createRepository(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", true, false, false, null, null); - qm.createRepository(RepositoryType.GITHUB, "github.com", "https://github.com", true, false, false, null, null); - } + qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", null, "https://fastapi.metacpan.org/v1/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.GEM, "rubygems.org", null, "https://rubygems.org/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.HEX, "hex.pm", null, "https://hex.pm/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.HACKAGE, "hackage.haskell.org", null, "https://hackage.haskell.org/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.MAVEN, "central", null, "https://repo1.maven.org/maven2/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.MAVEN, "atlassian-public", null, "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.MAVEN, "jboss-releases", null, "https://repository.jboss.org/nexus/content/repositories/releases/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.MAVEN, "clojars", null, "https://repo.clojars.org/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.MAVEN, "google-android", null, "https://maven.google.com/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.NIXPKGS, "nixpkgs-unstable", null, "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", true, false, false, null, null, null); + qm.createRepository(RepositoryType.NPM, "npm-public-registry", null, "https://registry.npmjs.org/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.PYPI, "pypi.org", null, "https://pypi.org/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.NUGET, "nuget-gallery", null, "https://api.nuget.org/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.COMPOSER, "packagist", null, "https://repo.packagist.org/", true, false, false, null, null, null); + qm.createRepository(RepositoryType.COMPOSER, "drupal8", null, "https://packages.drupal.org/8", false, false, false, null, null, null); + qm.createRepository(RepositoryType.CARGO, "crates.io", null, "https://crates.io", true, false, false, null, null, null); + qm.createRepository(RepositoryType.GO_MODULES, "proxy.golang.org", null, "https://proxy.golang.org", true, false, false, null, null, null); + qm.createRepository(RepositoryType.GITHUB, "github.com", null, "https://github.com", true, false, false, null, null, null); + } } /** diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index eeb5637615..a1924b8bb4 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1198,6 +1198,10 @@ public PaginatedResult getRepositories() { return getRepositoryQueryManager().getRepositories(); } + public Repository getRepository(String identifier) { + return getRepositoryQueryManager().getRepository(identifier); + } + public List getAllRepositories() { return getRepositoryQueryManager().getAllRepositories(); } @@ -1214,12 +1218,12 @@ public boolean repositoryExist(RepositoryType type, String identifier) { return getRepositoryQueryManager().repositoryExist(type, identifier); } - public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password) { - return getRepositoryQueryManager().createRepository(type, identifier, url, enabled, internal, isAuthenticationRequired, username, password); + public Repository createRepository(RepositoryType type, String identifier, String description, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password, String config) { + return getRepositoryQueryManager().createRepository(type, identifier, description, url, enabled, internal, isAuthenticationRequired, username, password, config); } - public Repository updateRepository(UUID uuid, String identifier, String url, boolean internal, boolean authenticationRequired, String username, String password, boolean enabled) { - return getRepositoryQueryManager().updateRepository(uuid, identifier, url, internal, authenticationRequired, username, password, enabled); + public Repository updateRepository(UUID uuid, String identifier, String description, String url, boolean internal, boolean authenticationRequired, String username, String password, boolean enabled, String config) { + return getRepositoryQueryManager().updateRepository(uuid, identifier, description, url, internal, authenticationRequired, username, password, enabled, config); } public RepositoryMetaComponent getRepositoryMetaComponent(RepositoryType repositoryType, String namespace, String name) { diff --git a/src/main/java/org/dependencytrack/persistence/RepositoryQueryManager.java b/src/main/java/org/dependencytrack/persistence/RepositoryQueryManager.java index 59094731f3..27615b91a5 100644 --- a/src/main/java/org/dependencytrack/persistence/RepositoryQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RepositoryQueryManager.java @@ -60,7 +60,6 @@ public class RepositoryQueryManager extends QueryManager implements IQueryManage super(pm, request); } - /** * Returns a list of all repositories. * @@ -133,10 +132,11 @@ public boolean repositoryExist(RepositoryType type, String identifier) { } /** - * Creates a new Repository. + * Creates a new Repository. Unless there alreaady is an existing Repository with the same type and identifier. * * @param type the type of repository * @param identifier a unique (to the type) identifier for the repo + * @param description description of the repository * @param url the URL to the repository * @param enabled if the repo is enabled or not * @param internal if the repo is internal or not @@ -145,7 +145,7 @@ public boolean repositoryExist(RepositoryType type, String identifier) { * @param password the password to access the (authenticated) repository with * @return the created Repository */ - public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password) { + public Repository createRepository(RepositoryType type, String identifier, String description, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password, String config) { if (repositoryExist(type, identifier)) { return null; } @@ -161,6 +161,7 @@ public Repository createRepository(RepositoryType type, String identifier, Strin final Repository repo = new Repository(); repo.setType(type); repo.setIdentifier(identifier); + repo.setDescription(description); repo.setUrl(url); repo.setResolutionOrder(order + 1); repo.setEnabled(enabled); @@ -176,6 +177,7 @@ public Repository createRepository(RepositoryType type, String identifier, Strin LOGGER.error("An error occurred while saving password in encrypted state"); } } + repo.setConfig(config); return persist(repo); } @@ -184,6 +186,7 @@ public Repository createRepository(RepositoryType type, String identifier, Strin * * @param uuid the uuid of the repository to update * @param identifier the identifier of the repository + * @param description description of the repository * @param url a url of the repository * @param internal specifies if the repository is internal * @param authenticationRequired if the repository needs authentication or not @@ -192,9 +195,10 @@ public Repository createRepository(RepositoryType type, String identifier, Strin * @param enabled specifies if the repository is enabled * @return the updated Repository */ - public Repository updateRepository(UUID uuid, String identifier, String url, boolean internal, boolean authenticationRequired, String username, String password, boolean enabled) { + public Repository updateRepository(UUID uuid, String identifier, String description, String url, boolean internal, boolean authenticationRequired, String username, String password, boolean enabled, String config) { final Repository repository = getObjectByUuid(Repository.class, uuid); repository.setIdentifier(identifier); + repository.setDescription(description); repository.setUrl(url); repository.setInternal(internal); repository.setAuthenticationRequired(authenticationRequired); @@ -207,6 +211,7 @@ public Repository updateRepository(UUID uuid, String identifier, String url, boo } repository.setEnabled(enabled); + repository.setConfig(config); return persist(repository); } diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index d9f0dc1668..e1203796b5 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -626,6 +626,25 @@ public synchronized VulnerabilityAlias synchronizeVulnerabilityAlias(final Vulne mustMatchAnyFilter += "vulnDbId != null"; params.put("vulnDbId", alias.getVulnDbId()); } + if (alias.getDrupalId() != null) { + if (filter.length() > 0) { + filter += " && "; + mustMatchAnyFilter += " || "; + } + filter += "(drupalId == :drupalId || drupalId == null)"; + mustMatchAnyFilter += "drupalId != null"; + params.put("drupalId", alias.getDrupalId()); + } + if (alias.getComposerId() != null) { + if (filter.length() > 0) { + filter += " && "; + mustMatchAnyFilter += " || "; + } + filter += "(composerId == :composerId || composerId == null)"; + mustMatchAnyFilter += "composerId != null"; + params.put("composerId", alias.getComposerId()); + } + if (alias.getInternalId() != null) { if (filter.length() > 0) { filter += " && "; @@ -676,6 +695,10 @@ public List getVulnerabilityAliases(Vulnerability vulnerabil query = pm.newQuery(VulnerabilityAlias.class, "snykId == :snykId"); } else if (Vulnerability.Source.VULNDB.name().equals(vulnerability.getSource())) { query = pm.newQuery(VulnerabilityAlias.class, "vulnDbId == :vulnDb"); + } else if (Vulnerability.Source.DRUPAL.name().equals(vulnerability.getSource())) { + query = pm.newQuery(VulnerabilityAlias.class, "drupalId == :drupalId"); + } else if (Vulnerability.Source.COMPOSER.name().equals(vulnerability.getSource())) { + query = pm.newQuery(VulnerabilityAlias.class, "composerId == :composerId"); } else { query = pm.newQuery(VulnerabilityAlias.class, "internalId == :internalId"); } @@ -711,6 +734,8 @@ public Map> getVulnerabilityAliases(fi case OSV -> "\"OSV_ID\" = :" + vulnIdParamName; case SNYK -> "\"SNYK_ID\" = :" + vulnIdParamName; case VULNDB -> "\"VULNDB_ID\" = :" + vulnIdParamName; + case DRUPAL -> "\"DRUPAL_ID\" = :" + vulnIdParamName; + case COMPOSER -> "\"COMPOSER_ID\" = :" + vulnIdParamName; default -> null; }; if (filter == null) { @@ -731,6 +756,8 @@ public Map> getVulnerabilityAliases(fi , "OSV_ID" , "SNYK_ID" , "VULNDB_ID" + , "DRUPAL_ID" + , "COMPOSER_ID" FROM "VULNERABILITYALIAS" WHERE %s """.formatted(vulnIdAndSource.hashCode(), filter)); @@ -758,6 +785,8 @@ public Map> getVulnerabilityAliases(fi alias.setOsvId((String) row[5]); alias.setSnykId((String) row[6]); alias.setVulnDbId((String) row[7]); + alias.setDrupalId((String) row[8]); + alias.setComposerId((String) row[9]); return Map.entry(vulnIdAndSource, alias); }) .collect(Collectors.groupingBy( diff --git a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java index bc7d1077e2..28dfa4f8c4 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RepositoryResource.java @@ -194,11 +194,13 @@ public Response createRepository(Repository jsonRepository) { final Repository repository = qm.createRepository( jsonRepository.getType(), StringUtils.trimToNull(jsonRepository.getIdentifier()), + StringUtils.trimToNull(jsonRepository.getDescription()), StringUtils.trimToNull(jsonRepository.getUrl()), jsonRepository.isEnabled(), jsonRepository.isInternal(), jsonRepository.isAuthenticationRequired(), - jsonRepository.getUsername(), jsonRepository.getPassword()); + jsonRepository.getUsername(), jsonRepository.getPassword(), + jsonRepository.getConfig()); return Response.status(Response.Status.CREATED).entity(repository).build(); } else { @@ -240,8 +242,8 @@ public Response updateRepository(Repository jsonRepository) { ? DataEncryption.encryptAsString(jsonRepository.getPassword()) : repository.getPassword(); - repository = qm.updateRepository(jsonRepository.getUuid(), repository.getIdentifier(), url, - jsonRepository.isInternal(), jsonRepository.isAuthenticationRequired(), jsonRepository.getUsername(), updatedPassword, jsonRepository.isEnabled()); + repository = qm.updateRepository(jsonRepository.getUuid(), repository.getIdentifier(), jsonRepository.getDescription(), url, + jsonRepository.isInternal(), jsonRepository.isAuthenticationRequired(), jsonRepository.getUsername(), updatedPassword, jsonRepository.isEnabled(), jsonRepository.getConfig()); return Response.ok(repository).build(); } catch (Exception e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("The specified repository password could not be encrypted.").build(); diff --git a/src/main/java/org/dependencytrack/tasks/TaskScheduler.java b/src/main/java/org/dependencytrack/tasks/TaskScheduler.java index 68d66b633c..0991a0d717 100644 --- a/src/main/java/org/dependencytrack/tasks/TaskScheduler.java +++ b/src/main/java/org/dependencytrack/tasks/TaskScheduler.java @@ -25,6 +25,7 @@ import alpine.model.IConfigProperty.PropertyType; import alpine.server.tasks.AlpineTaskScheduler; import org.dependencytrack.event.ClearComponentAnalysisCacheEvent; +import org.dependencytrack.event.ComposerAdvisoryMirrorEvent; import org.dependencytrack.event.DefectDojoUploadEventAbstract; import org.dependencytrack.event.FortifySscUploadEventAbstract; import org.dependencytrack.event.GitHubAdvisoryMirrorEvent; @@ -50,6 +51,7 @@ import static org.dependencytrack.model.ConfigPropertyConstants.KENNA_ENABLED; import static org.dependencytrack.model.ConfigPropertyConstants.KENNA_SYNC_CADENCE; import static org.dependencytrack.model.ConfigPropertyConstants.TASK_SCHEDULER_COMPONENT_ANALYSIS_CACHE_CLEAR_CADENCE; +import static org.dependencytrack.model.ConfigPropertyConstants.TASK_SCHEDULER_COMPOSER_MIRROR_CADENCE; import static org.dependencytrack.model.ConfigPropertyConstants.TASK_SCHEDULER_GHSA_MIRROR_CADENCE; import static org.dependencytrack.model.ConfigPropertyConstants.TASK_SCHEDULER_INTERNAL_COMPONENT_IDENTIFICATION_CADENCE; import static org.dependencytrack.model.ConfigPropertyConstants.TASK_SCHEDULER_LDAP_SYNC_CADENCE; @@ -71,44 +73,47 @@ public final class TaskScheduler extends AlpineTaskScheduler { // Holds an instance of TaskScheduler private static final TaskScheduler INSTANCE = new TaskScheduler(); + /** + * Private constructor. + */ + private TaskScheduler() { + try (QueryManager qm = new QueryManager()) { + // Creates a new event that executes every 6 hours (21600000) by default after an initial 10 second (10000) delay + scheduleEvent(new LdapSyncEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_LDAP_SYNC_CADENCE)); - /** - * Private constructor. - */ - private TaskScheduler() { - try (QueryManager qm = new QueryManager()) { - // Creates a new event that executes every 6 hours (21600000) by default after an initial 10 second (10000) delay - scheduleEvent(new LdapSyncEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_LDAP_SYNC_CADENCE)); + // Creates a new event that executes every 24 hours (86400000) by default after an initial 10 second (10000) delay + scheduleEvent(new GitHubAdvisoryMirrorEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_GHSA_MIRROR_CADENCE)); + + // Creates a new event that executes every 24 hours (86400000) by default after an initial 10 second (10000) delay + scheduleEvent(new OsvMirrorEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_OSV_MIRROR_CADENCE)); - // Creates a new event that executes every 24 hours (86400000) by default after an initial 10 second (10000) delay - scheduleEvent(new GitHubAdvisoryMirrorEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_GHSA_MIRROR_CADENCE)); + // Creates a new event that executes every 24 hours (86400000) by default after an initial 1 minute (60000) delay + scheduleEvent(new NistMirrorEvent(), 60000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_NIST_MIRROR_CADENCE)); - // Creates a new event that executes every 24 hours (86400000) by default after an initial 10 second (10000) delay - scheduleEvent(new OsvMirrorEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_OSV_MIRROR_CADENCE)); + // Creates a new event that executes every 24 hours (86400000) by default after an initial 1 minute (60000) delay + scheduleEvent(new VulnDbSyncEvent(), 60000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_VULNDB_MIRROR_CADENCE)); - // Creates a new event that executes every 24 hours (86400000) by default after an initial 1 minute (60000) delay - scheduleEvent(new NistMirrorEvent(), 60000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_NIST_MIRROR_CADENCE)); + // Creates a new event that executes every 1 hour (3600000) by default after an initial 10 second (10000) delay + scheduleEvent(new PortfolioMetricsUpdateEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_PORTFOLIO_METRICS_UPDATE_CADENCE)); - // Creates a new event that executes every 24 hours (86400000) by default after an initial 1 minute (60000) delay - scheduleEvent(new VulnDbSyncEvent(), 60000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_VULNDB_MIRROR_CADENCE)); + // Creates a new event that executes every 1 hour (3600000) by default after an initial 10 second (10000) delay + scheduleEvent(new VulnerabilityMetricsUpdateEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_VULNERABILITY_METRICS_UPDATE_CADENCE)); - // Creates a new event that executes every 1 hour (3600000) by default after an initial 10 second (10000) delay - scheduleEvent(new PortfolioMetricsUpdateEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_PORTFOLIO_METRICS_UPDATE_CADENCE)); + // Creates a new event that executes every 24 hours (86400000) by default after an initial 6 hour (21600000) delay + scheduleEvent(new PortfolioVulnerabilityAnalysisEvent(), 21600000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_PORTFOLIO_VULNERABILITY_ANALYSIS_CADENCE)); - // Creates a new event that executes every 1 hour (3600000) by default after an initial 10 second (10000) delay - scheduleEvent(new VulnerabilityMetricsUpdateEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_VULNERABILITY_METRICS_UPDATE_CADENCE)); + // Creates a new event that executes every 24 hours (86400000) by default after an initial 1 hour (3600000) delay + scheduleEvent(new RepositoryMetaEvent(), 3600000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_REPOSITORY_METADATA_FETCH_CADENCE)); - // Creates a new event that executes every 24 hours (86400000) by default after an initial 6 hour (21600000) delay - scheduleEvent(new PortfolioVulnerabilityAnalysisEvent(), 21600000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_PORTFOLIO_VULNERABILITY_ANALYSIS_CADENCE)); + // Creates a new event that executes every 6 hours (21600000) by default after an initial 1 hour (3600000) delay + scheduleEvent(new InternalComponentIdentificationEvent(), 3600000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_INTERNAL_COMPONENT_IDENTIFICATION_CADENCE)); - // Creates a new event that executes every 24 hours (86400000) by default after an initial 1 hour (3600000) delay - scheduleEvent(new RepositoryMetaEvent(), 3600000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_REPOSITORY_METADATA_FETCH_CADENCE)); + // Creates a new event that executes every 72 hours (259200000) by default after an initial 10 second (10000) delay + scheduleEvent(new ClearComponentAnalysisCacheEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_COMPONENT_ANALYSIS_CACHE_CLEAR_CADENCE)); - // Creates a new event that executes every 6 hours (21600000) by default after an initial 1 hour (3600000) delay - scheduleEvent(new InternalComponentIdentificationEvent(), 3600000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_INTERNAL_COMPONENT_IDENTIFICATION_CADENCE)); + // Creates a new event that executes every 24 hours (86400000) by default after an initial 10 second (10000) delay + scheduleEvent(new ComposerAdvisoryMirrorEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_COMPOSER_MIRROR_CADENCE)); - // Creates a new event that executes every 72 hours (259200000) by default after an initial 10 second (10000) delay - scheduleEvent(new ClearComponentAnalysisCacheEvent(), 10000, getCadenceConfigPropertyValueInMilliseconds(qm, TASK_SCHEDULER_COMPONENT_ANALYSIS_CACHE_CLEAR_CADENCE)); } // Configurable tasks diff --git a/src/main/java/org/dependencytrack/tasks/repositories/ComposerAdvisoryMirrorTask.java b/src/main/java/org/dependencytrack/tasks/repositories/ComposerAdvisoryMirrorTask.java new file mode 100644 index 0000000000..74357769bd --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/repositories/ComposerAdvisoryMirrorTask.java @@ -0,0 +1,424 @@ +/* + * This file is part of Dependency-Track. + * + * 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 + * + * http://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. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.tasks.repositories; + +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.Map.Entry; + +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.event.ComposerAdvisoryMirrorEvent; +import org.dependencytrack.event.IndexEvent; +import org.dependencytrack.model.Repository; +import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.Vulnerability.Source; +import org.dependencytrack.model.VulnerabilityAlias; +import org.dependencytrack.model.VulnerableSoftware; +import org.dependencytrack.notification.NotificationConstants; +import org.dependencytrack.notification.NotificationGroup; +import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.parser.composer.ComposerAdvisoryParser; +import org.dependencytrack.parser.composer.model.ComposerAdvisory; +import org.dependencytrack.persistence.QueryManager; +import org.json.JSONObject; + +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import com.github.packageurl.PackageURLBuilder; + +import alpine.common.logging.Logger; +import alpine.event.framework.Event; +import alpine.event.framework.LoggableSubscriber; +import alpine.notification.Notification; +import alpine.notification.NotificationLevel; + +public class ComposerAdvisoryMirrorTask implements LoggableSubscriber { + + private static final Logger LOGGER = Logger.getLogger(ComposerAdvisoryMirrorTask.class); + + private boolean mirroredWithoutErrors = true; + + /** + * {@inheritDoc} + */ + public void inform(final Event e) { + if (e instanceof ComposerAdvisoryMirrorEvent) { + final long start = System.currentTimeMillis(); + LOGGER.info("Starting Composer Advisory mirroring task"); + + try (final var qm = new QueryManager()) { + for (final Repository repository : qm.getAllRepositoriesOrdered(RepositoryType.COMPOSER)) { + // Should we try catch all exceptions to make sure notification is sent? + mirroredWithoutErrors &= mirrorAdvisories(qm, repository); + } + } + + final long end = System.currentTimeMillis(); + LOGGER.info("Composer Advisory mirroring complete"); + LOGGER.info("Time spent (total): " + (end - start) + "ms"); + + if (mirroredWithoutErrors) { + Notification.dispatch(new Notification() + .scope(NotificationScope.SYSTEM) + .group(NotificationGroup.DATASOURCE_MIRRORING) + .title(NotificationConstants.Title.COMPOSER_ADVISORY_MIRROR) + .content("Mirroring of Composer Advisories completed successfully") + .level(NotificationLevel.INFORMATIONAL)); + } else { + Notification.dispatch(new Notification() + .scope(NotificationScope.SYSTEM) + .group(NotificationGroup.DATASOURCE_MIRRORING) + .title(NotificationConstants.Title.COMPOSER_ADVISORY_MIRROR) + .content( + "An error occurred mirroring the contents of Composer Advisories. Check log for details.") + .level(NotificationLevel.ERROR)); + } + } + Event.dispatch(new IndexEvent(IndexEvent.Action.COMMIT, Vulnerability.class)); + } + + protected boolean mirrorAdvisories(QueryManager qm, Repository repository) { + if (!repository.isEnabled()) { + return true; + } + boolean isAdvisoryMirroringEnabled = false; + boolean isAliasSyncEnabled = false; + + if (repository.getConfig() != null) { + final JSONObject config = new JSONObject(repository.getConfig()); + isAdvisoryMirroringEnabled = config + .optBoolean("advisoryMirroringEnabled", false); + isAliasSyncEnabled = config + .optBoolean("advisoryAliasSyncEnabled", true); + if (!isAdvisoryMirroringEnabled) { + LOGGER.info( + "Advisory mirroring is disabled for repository " + repository.getUrl()); + } + } + + boolean result = true; + // Vulnerability mirroring builds on the Composer meta analyzer + // To avoid duplicating lots of code or having to extract alle common parts and + // error handling, we just create an analyzer and let it do the work + ComposerMetaAnalyzer composerMetaAnalyzer = new ComposerMetaAnalyzer(); + composerMetaAnalyzer.setRepositoryId(repository.getIdentifier()); + composerMetaAnalyzer.setRepositoryBaseUrl(repository.getUrl()); + composerMetaAnalyzer.setRepositoryUsernameAndPassword(repository.getUsername(), repository.getPassword()); + + LOGGER.info("Mirorring Composer Advisories from " + repository.getUrl()); + JSONObject jsonAdvisories = composerMetaAnalyzer.retrieveAdvisories(); + + if (jsonAdvisories == null) { + return false; + } + + final List composerAdvisories = ComposerAdvisoryParser.parseAdvisoryFeed(jsonAdvisories); + for (final ComposerAdvisory advisory : composerAdvisories) { + result &= processAdvisory(qm, advisory, isAliasSyncEnabled); + } + + return result; + } + + /** + * Synchronizes the Composer Advisories to the database. + * @param qm + * @param advisories the advisories to synchronize + * @param syncAliases + */ + boolean processAdvisory(QueryManager qm, final ComposerAdvisory advisory, boolean syncAliases) { + LOGGER.debug("Synchronizing Composer advisory: " + advisory.getAdvisoryId()); + + final Vulnerability mappedVulnerability = mapComposerAdvisoryToVulnerability(advisory, syncAliases); + final List vsListOld = qm.detach(qm.getVulnerableSoftwareByVulnId( + mappedVulnerability.getSource(), mappedVulnerability.getVulnId())); + + final Vulnerability existingVulnerability = qm.getVulnerabilityByVulnId(mappedVulnerability.getSource(), + mappedVulnerability.getVulnId()); + + final Vulnerability.Source vulnerabilitySource = Vulnerability.Source + .valueOf(mappedVulnerability.getSource()); + + Vulnerability synchronizedVulnerability = existingVulnerability; + if (shouldUpdateExistingVulnerability(existingVulnerability, vulnerabilitySource)) { + synchronizedVulnerability = qm.synchronizeVulnerability(mappedVulnerability, false); + if (synchronizedVulnerability == null) + return true; + } + + if (syncAliases && mappedVulnerability.getAliases() != null && mappedVulnerability.getAliases().size() > 0) { + for (VulnerabilityAlias alias : mappedVulnerability.getAliases()) { + qm.synchronizeVulnerabilityAlias(alias); + } + } + + LOGGER.debug("Updating vulnerable software for advisory: " + advisory.getAdvisoryId()); + List vsList = mapVulnerabilityToVulnerableSoftware(qm, advisory); + qm.persist(vsList); + final Vulnerability finalSynchronizedVulnerability = synchronizedVulnerability; + final Vulnerability.Source attributionSource = vulnerabilitySource == Vulnerability.Source.DRUPAL? Vulnerability.Source.DRUPAL : Vulnerability.Source.COMPOSER; + vsList.forEach(vs -> qm.updateAffectedVersionAttribution(finalSynchronizedVulnerability, vs, + attributionSource)); + vsList = qm.reconcileVulnerableSoftware(synchronizedVulnerability, vsListOld, vsList, + attributionSource); + synchronizedVulnerability.setVulnerableSoftware(vsList); + qm.persist(synchronizedVulnerability); + return true; + } + + private boolean shouldUpdateExistingVulnerability(Vulnerability existingVulnerability, + Vulnerability.Source vulnerabilitySource) { + /* + * Compose Advisories can have their own Id (PKSA-xxxx-yyy) or an Id from an + * authoritive source (CVE-xxxx-yyy, GHSA-xxxx-yyy, ...) + * I haven't seen any Composer Advisory with a CVE or GHSA id, but it is + * possible. + * Make sure that we don't overwrite data of the authoritative source + * Similar to what is done for Osv Mirroring + * Please note that Drupal is also considered authoritative source, but provided + * by Composer here in this task + */ + return (EnumSet.of(Vulnerability.Source.COMPOSER, Vulnerability.Source.DRUPAL).contains(vulnerabilitySource)) + || (existingVulnerability == null); + } + + public static String extractVulnId(ComposerAdvisory composerAdvisory) { + // Currently seen DRUPAL and COMPOSER, but for composer we need to look for other fields + Source sourceFromId = Vulnerability.Source.resolve(composerAdvisory.getAdvisoryId()); + if (sourceFromId != null && !EnumSet.of(Vulnerability.Source.UNKNOWN, Vulnerability.Source.COMPOSER).contains(sourceFromId)) { + return composerAdvisory.getAdvisoryId(); + } + + // Currently only used for GHSA and pointers to Friends Of PHP advisories, which is not a valid source + Source sourceFromRemoteId = Vulnerability.Source.resolve(composerAdvisory.getRemoteId()); + if (sourceFromRemoteId != null && sourceFromRemoteId != Vulnerability.Source.UNKNOWN) { + return composerAdvisory.getRemoteId(); + } + + // Some Advisories from Friends Of PHP have a GHSA, which is leading + for (String possibleAlias : composerAdvisory.getSources().values()) { + Source sourceFromPossibleAlias = Vulnerability.Source.resolve(possibleAlias); + if (sourceFromPossibleAlias != null && sourceFromPossibleAlias != Vulnerability.Source.UNKNOWN) { + return possibleAlias; + } + } + + // Use CVE, but ensure it's valid. You never know with these Composer repositories + Source sourceFromCve = Vulnerability.Source.resolve(composerAdvisory.getCve()); + if (sourceFromCve != null && sourceFromCve == Vulnerability.Source.NVD) { + return composerAdvisory.getCve(); + } + + // Wordt case will result in a PKSA as id, similar to OSV. + return composerAdvisory.getAdvisoryId(); + } + + protected Vulnerability mapComposerAdvisoryToVulnerability(final ComposerAdvisory composerAdvisory, final boolean syncAliases) { + final Vulnerability vuln = new Vulnerability(); + + vuln.setVulnId(extractVulnId(composerAdvisory)); + vuln.setSource(Vulnerability.Source.resolve(vuln.getVulnId())); + + String description = composerAdvisory.getTitle() + " in " + composerAdvisory.getPackageName() + " " + + composerAdvisory.getAffectedVersions() + "\n\n"; + List references = new ArrayList<>(); + references.add(composerAdvisory.getLink()); + for (Entry source : composerAdvisory.getSources().entrySet()) { + if (source.getKey().equalsIgnoreCase("github")) { + references.add("https://github.com/advisories/" + source.getValue()); + } else if (source.getKey().equalsIgnoreCase("friendsofphp/security-advisories")) { + references.add("https://github.com/FriendsOfPHP/security-advisories/blob/master/" + source.getValue()); + } else { + description += "\nUnmapped source: " + source.getKey() + " : " + source.getValue() + "\n"; + } + } + references.add(composerAdvisory.getComposerRepository()); + + if (!references.isEmpty()) { + final StringBuilder sb = new StringBuilder(); + for (String ref : references) { + // Convert reference to Markdown format; + sb.append("* [").append(ref).append("](").append(ref).append(")\n"); + } + vuln.setReferences(sb.toString()); + } + + vuln.setDescription(description); + vuln.setTitle(StringUtils.abbreviate(composerAdvisory.getTitle(), "...", 255)); + if (composerAdvisory.getReportedAt() != null) { + vuln.setPublished(Date.from(composerAdvisory.getReportedAt().toInstant(ZoneOffset.UTC))); + // Should we leave Updated null? + vuln.setUpdated(Date.from(composerAdvisory.getReportedAt().toInstant(ZoneOffset.UTC))); + } + + if (composerAdvisory.getAffectedVersions() != null + && composerAdvisory.getAffectedVersions().length() > 255) { + // https://github.com/DependencyTrack/dependency-track/issues/4512 + LOGGER.warn("Affected versions for " + composerAdvisory.getAdvisoryId() + + " is too long. Truncating to 255 characters."); + vuln.setVulnerableVersions(StringUtils.abbreviate(composerAdvisory.getAffectedVersions(), "...", 255)); + } else { + vuln.setVulnerableVersions(composerAdvisory.getAffectedVersions()); + } + + if (composerAdvisory.getSeverity() != null) { + if (composerAdvisory.getSeverity().equalsIgnoreCase("CRITICAL")) { + vuln.setSeverity(Severity.CRITICAL); + } else if (composerAdvisory.getSeverity().equalsIgnoreCase("HIGH")) { + vuln.setSeverity(Severity.HIGH); + } else if (composerAdvisory.getSeverity().equalsIgnoreCase("MEDIUM")) { + vuln.setSeverity(Severity.MEDIUM); + } else if (composerAdvisory.getSeverity().equalsIgnoreCase("LOW")) { + vuln.setSeverity(Severity.LOW); + } else { + vuln.setSeverity(Severity.UNASSIGNED); + } + } else { + vuln.setSeverity(Severity.UNASSIGNED); + } + + if (syncAliases) { + VulnerabilityAlias alias = new VulnerabilityAlias(); + alias.setAliasFromVulnId(vuln.getVulnId()); + alias.setAliasFromVulnId(composerAdvisory.getCve()); + alias.setAliasFromVulnId(composerAdvisory.getRemoteId()); + for (String possibleAlias : composerAdvisory.getSources().values()) { + alias.setAliasFromVulnId(possibleAlias); + } + + if (alias.countIdentifiers() > 1) { + vuln.setAliases(List.of(alias)); + } + } + + return vuln; + } + + /** + * Helper method that maps an GitHub Vulnerability object to a Dependency-Track + * VulnerableSoftware object. + * + * @param qm a QueryManager + * @param vuln the GitHub Vulnerability to map + * @return a Dependency-Track VulnerableSoftware object + */ + protected List mapVulnerabilityToVulnerableSoftware(final QueryManager qm, + final ComposerAdvisory advisory) { + final List vsList = new ArrayList<>(); + try { + final PackageURL purl = generatePurlFromComposerAdvisory(advisory); + if (purl == null) + return null; + + if (advisory.getAffectedVersions() != null) { + // regex splitters copied from Composer Version Parser + LOGGER.trace("Parsing version ranges for " + advisory.getPackageEcosystem() + " : " + + advisory.getPackageName() + " : " + advisory.getAffectedVersions()); + String[] ranges = Arrays.stream(advisory.getAffectedVersions().split("\\s*\\|\\|?\\s*")) + .map(String::trim).toArray(String[]::new); + + for (String range : ranges) { + String versionStartIncluding = null; + String versionStartExcluding = null; + String versionEndIncluding = null; + String versionEndExcluding = null; + // Split by both ',' and ' ' + String[] parts = Arrays.stream(range.split("(?< ,]) *(?=")) { + versionStartIncluding = part.replace(">=", "").trim(); + } else if (part.startsWith(">")) { + versionStartExcluding = part.replace(">", "").trim(); + } else if (part.startsWith("<=")) { + versionEndIncluding = part.replace("<=", "").trim(); + } else if (part.startsWith("<")) { + versionEndExcluding = part.replace("<", "").trim(); + } else if (part.startsWith("=")) { + versionStartIncluding = part.replace("=", "").trim(); + versionEndIncluding = part.replace("=", "").trim(); + } else if (part.trim().equals("*")) { + // Drupal sometimes uses * to indicate all versions are vulnerable for abandoned plugins + // Since we don't have a "deprecated" or "endoflife" or "unsupported" or "abandoned" flag, we do this: + versionEndExcluding = "999.999.999"; + } else { + // No operator, so it's a single version. Or garbage. But since none of the parts are checked for formatting, we don't check neither + // Drupal uses this, for example "8.1.0" + versionStartIncluding = part; + versionEndIncluding = part; + } + } + VulnerableSoftware vs = qm.getVulnerableSoftwareByPurl(purl.getType(), purl.getNamespace(), purl.getName(), purl.getVersion(), + versionEndExcluding, versionEndIncluding, versionStartExcluding, versionStartIncluding); + if (vs != null) { + if (!vsList.contains(vs)) { + vsList.add(vs); + continue; + } + } + + vs = new VulnerableSoftware(); + vs.setVulnerable(true); + vs.setPurlType(purl.getType()); + vs.setPurlNamespace(purl.getNamespace()); + vs.setPurlName(purl.getName()); + vs.setPurl(purl.canonicalize()); + vs.setVersionStartIncluding(versionStartIncluding); + vs.setVersionStartExcluding(versionStartExcluding); + vs.setVersionEndIncluding(versionEndIncluding); + vs.setVersionEndExcluding(versionEndExcluding); + + vsList.add(vs); + } + } + LOGGER.trace("Resulting VulnerableSoftware: " + vsList); + return vsList; + } catch (MalformedPackageURLException e) { + LOGGER.warn("Unable to create purl from Composer Vulnerability. Skipping " + advisory.getPackageEcosystem() + + " : " + advisory.getPackageName() + " for: " + advisory.getAdvisoryId()); + } + return null; + } + + private PackageURL generatePurlFromComposerAdvisory(final ComposerAdvisory vuln) + throws MalformedPackageURLException { + final String[] parts = vuln.getPackageName().split("/"); + final String namespace = String.join("/", Arrays.copyOfRange(parts, 0, parts.length - 1)); + return PackageURLBuilder.aPackageURL().withType(vuln.getPackageEcosystem()).withNamespace(namespace) + .withName(parts[parts.length - 1]).build(); + } + + protected void handleRequestException(final Logger logger, final Exception e) { + logger.error("Request failure", e); + Notification.dispatch(new Notification() + .scope(NotificationScope.SYSTEM) + .group(NotificationGroup.ANALYZER) + .title(NotificationConstants.Title.ANALYZER_ERROR) + .content( + "An error occurred while communicating with a vulnerability intelligence source. Check log for details. " + + e.getMessage()) + .level(NotificationLevel.ERROR)); + } +} diff --git a/src/main/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzer.java index 7f47c0c35e..e1da36b54b 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzer.java @@ -181,7 +181,7 @@ public MetaModel analyze(final Component component) { return analyzeFromMetadataUrl(meta, component, PACKAGE_META_DATA_PATH_PATTERN_V1); } - private JSONObject getRepoRoot() { + public JSONObject getRepoRoot() { // Code mimicksed from // https://github.com/composer/composer/blob/main/src/Composer/Repository/ComposerRepository.php // Retrieve packages.json file, which must be present even for V1 repositories @@ -203,6 +203,7 @@ private JSONObject getRepoRootFromUrl(String packageJsonUrl) { if (JsonUtil.isBlankJson(packageJsonString)) { LOGGER.warn("%s: Empty packages.json from %s".formatted(this.repositoryId, packageJsonUrl)); } else { + LOGGER.debug("packages.json retrieved from " + packageJsonUrl); repoRoot = new JSONObject(packageJsonString); } } @@ -402,6 +403,41 @@ private MetaModel analyzePackageVersions(final MetaModel meta, Component compone return meta; } + + public JSONObject retrieveAdvisories() { + JSONObject repoRoot = getRepoRoot(); + if (repoRoot == null) { + return null; + } + + if (repoRoot.optJSONObject("security-advisories") == null) { + LOGGER.info("No security advisory Api Url found in repository " + baseUrl); + return null; + } + + String advisoryUrl = repoRoot.getJSONObject("security-advisories").optString("api-url"); + if (advisoryUrl == null || advisoryUrl.isEmpty()) { + LOGGER.info("No security advisory Api Url found in repository " + baseUrl); + return null; + } + LOGGER.debug("Retrieving Composert Security Advisories from " + advisoryUrl); + // No incremental updates yet + String advisoryJsonUrl = UriBuilder.fromUri(advisoryUrl).queryParam("updatedSince", 100).build().toString(); + try (CloseableHttpResponse response = processHttpRequest(advisoryJsonUrl)) { + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + LOGGER.error("An error was encountered retrieving advisories with HTTP Status : " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase()); + return null; + } else { + String responseString = EntityUtils.toString(response.getEntity()); + // LOGGER.debug("response from composer repository: \n" + responseString); + return new JSONObject(responseString); + } + } catch (IOException ex) { + LOGGER.error("Exception while executing Http client request", ex); + } + return null; + } + private static String stripLeadingV(String s) { return s.startsWith("v") || s.startsWith("V") ? s.substring(1) diff --git a/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java index cd6c2a80c1..a1680a25d6 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/IMetaAnalyzer.java @@ -165,7 +165,6 @@ public void setRepositoryBaseUrl(String baseUrl) { @Override public void setRepositoryUsernameAndPassword(String username, String password) { - } @Override diff --git a/src/main/java/org/dependencytrack/util/JsonUtil.java b/src/main/java/org/dependencytrack/util/JsonUtil.java index 6a6e688edd..f23dfb5b27 100644 --- a/src/main/java/org/dependencytrack/util/JsonUtil.java +++ b/src/main/java/org/dependencytrack/util/JsonUtil.java @@ -23,10 +23,13 @@ import java.math.BigInteger; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; + import org.apache.commons.lang3.StringUtils; -public final class JsonUtil { +import alpine.common.logging.Logger; +public final class JsonUtil { + private static final Logger LOGGER = Logger.getLogger(JsonUtil.class); /** * Private constructor. */ @@ -67,6 +70,7 @@ public static ZonedDateTime jsonStringToTimestamp(final String s) { try { return ZonedDateTime.parse(s); } catch (DateTimeParseException e) { + LOGGER.trace("Unabled to parse ZonedDateTime: " + s); return null; } } diff --git a/src/test/java/org/dependencytrack/model/VulnerabilityAliasTest.java b/src/test/java/org/dependencytrack/model/VulnerabilityAliasTest.java index 69d07ebb04..2631b516bd 100644 --- a/src/test/java/org/dependencytrack/model/VulnerabilityAliasTest.java +++ b/src/test/java/org/dependencytrack/model/VulnerabilityAliasTest.java @@ -15,6 +15,8 @@ public void testCopyFieldsFrom() { alias.setSnykId("someSnykId"); alias.setGsdId("someGsdId"); alias.setVulnDbId("someVulnDbId"); + alias.setDrupalId("someDrupalId"); + alias.setComposerId("someComposerId"); alias.setInternalId("someInternalId"); var other = new VulnerabilityAlias(); @@ -24,6 +26,8 @@ public void testCopyFieldsFrom() { other.setOsvId("anotherOsvId"); other.setSnykId("anotherSnykId"); other.setGsdId("anotherGsdId"); + other.setDrupalId("someDrupalId"); + other.setComposerId("someComposerId"); other.setInternalId("anotherInternalId"); other.setVulnDbId(null); @@ -36,6 +40,8 @@ public void testCopyFieldsFrom() { Assert.assertEquals(other.getSnykId(), alias.getSnykId()); Assert.assertEquals(other.getGsdId(), alias.getGsdId()); Assert.assertEquals(other.getInternalId(), alias.getInternalId()); + Assert.assertEquals(other.getDrupalId(), alias.getDrupalId()); + Assert.assertEquals(other.getComposerId(), alias.getComposerId()); // null does not overwrite existing value Assert.assertEquals(alias.getVulnDbId(), alias.getVulnDbId()); diff --git a/src/test/java/org/dependencytrack/parser/composer/ComposerAdvisoryParserTest.java b/src/test/java/org/dependencytrack/parser/composer/ComposerAdvisoryParserTest.java new file mode 100644 index 0000000000..e57fd6c591 --- /dev/null +++ b/src/test/java/org/dependencytrack/parser/composer/ComposerAdvisoryParserTest.java @@ -0,0 +1,260 @@ +package org.dependencytrack.parser.composer; + +import java.io.IOException; + +import org.dependencytrack.parser.composer.model.ComposerAdvisory; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Test; + +public class ComposerAdvisoryParserTest { + + public final static JSONObject VULN_DRUPAL = new JSONObject(""" + { + "advisoryId": "SA-CORE-2018-003", + "packageName": "drupal/core", + "title": "Drupal core - Moderately critical - Cross Site Scripting - SA-CORE-2018-003", + "link": "https://www.drupal.org/sa-core-2018-003", + "cve": "CVE-2018-9861", + "affectedVersions": "\u003E= 8.0.0 \u003C8.4.7 || \u003E=8.5.0 \u003C8.5.2", + "reportedAt": "2018-04-18 15:34:09", + "composerRepository": "https://packagist.org/", + "sources": [ + { + "name": "Drupal core - Moderately critical - Cross Site Scripting - SA-CORE-2018-003", + "remoteId": "SA-CORE-2018-003" + } + ] + } + """); + + public final static JSONObject VULN_GHSA = new JSONObject(""" + { + "advisoryId": "PKSA-228k-hrjg-43zp", + "packageName": "magento/community-edition", + "remoteId": "GHSA-297f-r9w7-w492", + "title": "Magento Improper input validation vulnerability", + "link": "https://github.com/advisories/GHSA-297f-r9w7-w492", + "cve": "CVE-2022-42344", + "affectedVersions": "=2.4.4|\u003E=2.4.0,\u003C2.4.3-p3|\u003C2.3.7-p4", + "source": "GitHub", + "reportedAt": "2022-10-20 19:00:29", + "composerRepository": "https://packagist.org", + "severity": "high", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-297f-r9w7-w492" + } + ] + }, + """); + + public final static JSONObject VULN_FOP_NO_CVE = new JSONObject( + """ + { + "advisoryId": "PKSA-n8hw-tywm-xrh7", + "packageName": "drupal/core", + "remoteId": "drupal/core/2019-12-18-1.yaml", + "title": "Drupal core - Moderately critical - Denial of Service - SA-CORE-2019-009", + "link": "https://www.drupal.org/sa-core-2019-009", + "cve": null, + "affectedVersions": "\u003E=8.0.0,\u003C8.1.0|\u003E=8.1.0,\u003C8.2.0|\u003E=8.2.0,\u003C8.3.0|\u003E=8.3.0,\u003C8.4.0|\u003E=8.4.0,\u003C8.5.0|\u003E=8.5.0,\u003C8.6.0|\u003E=8.6.0,\u003C8.7.0|\u003E=8.7.0,\u003C8.7.11|\u003E=8.8.0,\u003C8.8.1", + "source": "FriendsOfPHP/security-advisories", + "reportedAt": "2019-12-18 00:00:00", + "composerRepository": "https://packagist.org", + "severity": "critical", + "sources": [ + { + "name": "FriendsOfPHP/security-advisories", + "remoteId": "drupal/core/2019-12-18-1.yaml" + }, + { + "name": "GitHub", + "remoteId": "GHSA-7v68-3pr5-h3cr" + } + ] + } + """); + + public final static JSONObject VULN_FOP = new JSONObject( + """ + { + "advisoryId": "PKSA-p9s6-dthp-ws2d", + "packageName": "simplesamlphp/saml2", + "remoteId": "simplesamlphp/saml2/CVE-2016-9814.yaml", + "title": "Incorrect signature verification", + "link": "https://simplesamlphp.org/security/201612-01", + "cve": "CVE-2016-9814", + "affectedVersions": "\u003C1.8.1|\u003E=1.9.0,\u003C1.9.1|\u003E=1.10,\u003C1.10.3|\u003E=2.0,\u003C2.3.3", + "source": "FriendsOfPHP/security-advisories", + "reportedAt": "2016-11-29 13:12:44", + "composerRepository": "https://packagist.org", + "severity": "critical", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-r8v4-7vwj-983x" + }, + { + "name": "FriendsOfPHP/security-advisories", + "remoteId": "simplesamlphp/saml2/CVE-2016-9814.yaml" + } + ] + }, + """); + + // Theoretical case to prepare for other repositories + public final static JSONObject VULN_FOP_CVE = new JSONObject( + """ + { + "advisoryId": "PKSA-p9s6-dthp-ws2d", + "packageName": "simplesamlphp/saml2", + "remoteId": "simplesamlphp/saml2/CVE-2016-9814.yaml", + "title": "Incorrect signature verification", + "link": "https://simplesamlphp.org/security/201612-01", + "cve": "CVE-2016-9814", + "affectedVersions": "\u003C1.8.1|\u003E=1.9.0,\u003C1.9.1|\u003E=1.10,\u003C1.10.3|\u003E=2.0,\u003C2.3.3", + "source": "FriendsOfPHP/security-advisories", + "reportedAt": "2016-11-29 13:12:44", + "composerRepository": "https://packagist.org", + "severity": "critical", + "sources": [ + { + "name": "FriendsOfPHP/security-advisories", + "remoteId": "simplesamlphp/saml2/CVE-2016-9814.yaml" + } + ] + }, + """); + + + // Hypothetical vulnerability to future proof our parser + public final static JSONObject VULN_COMPOSER = new JSONObject( + """ + { + "advisoryId": "PKSA-m9t7-ggb8-abcd", + "packageName": "social/media", + "remoteId": null, + "title": "File REST resource does not properly validate", + "link": "https://www.somesource.org/vulnerability/1234", + "cve": null, + "affectedVersions": "\u003E=8.0,\u003C8.1.0|\u003E=8.1.0,\u003C8.2.0|\u003E=8.2.0,\u003C8.3.0|\u003E=8.3.0,\u003C8.3.4", + "source": "somesource", + "reportedAt": "2017-06-21 18:13:27", + "composerRepository": "https://packagist.org", + "severity": "medium", + "sources": [] + } + """); + + public final static JSONObject VULN_DRUPAL_INVALID_TIME = new JSONObject( + """ + { + "advisoryId": "PKSA-n8hw-tywm-xrh7", + "packageName": "drupal/core", + "remoteId": "drupal/core/2019-12-18-1.yaml", + "title": "Drupal core - Moderately critical - Denial of Service - SA-CORE-2019-009", + "link": "https://www.drupal.org/sa-core-2019-009", + "cve": null, + "affectedVersions": "\u003E=8.0.0,\u003C8.1.0|\u003E=8.1.0,\u003C8.2.0|\u003E=8.2.0,\u003C8.3.0|\u003E=8.3.0,\u003C8.4.0|\u003E=8.4.0,\u003C8.5.0|\u003E=8.5.0,\u003C8.6.0|\u003E=8.6.0,\u003C8.7.0|\u003E=8.7.0,\u003C8.7.11|\u003E=8.8.0,\u003C8.8.1", + "source": "FriendsOfPHP/security-advisories", + "reportedAt": "2019-122222-18 00:00:00", + "composerRepository": "https://packagist.org", + "severity": "critical", + "sources": [ + { + "name": "FriendsOfPHP/security-advisories", + "remoteId": "drupal/core/2019-12-18-1.yaml" + }, + { + "name": "GitHub", + "remoteId": "GHSA-7v68-3pr5-h3cr" + } + ] + } + """); + + public final static JSONObject VULN_WILDCARD_ALL = new JSONObject(""" + { + "advisoryId": "PKSA-n8hw-tywm-xrh7", + "packageName": "drupal/core", + "remoteId": "drupal/core/2019-12-18-1.yaml", + "title": "Drupal core - Moderately critical - Denial of Service - SA-CORE-2019-009", + "link": "https://www.drupal.org/sa-core-2019-009", + "cve": null, + "affectedVersions": "*", + "source": "FriendsOfPHP/security-advisories", + "reportedAt": "2019-122222-18 00:00:00", + "composerRepository": "https://packagist.org", + "severity": "critical", + "sources": [ + { + "name": "FriendsOfPHP/security-advisories", + "remoteId": "drupal/core/2019-12-18-1.yaml" + }, + { + "name": "GitHub", + "remoteId": "GHSA-7v68-3pr5-h3cr" + } + ] + } + """); + + + public final static JSONObject VULN_EXACT_VERSION = new JSONObject(""" + { + "advisoryId": "PKSA-n8hw-tywm-xrh7", + "packageName": "drupal/core", + "remoteId": "drupal/core/2019-12-18-1.yaml", + "title": "Drupal core - Moderately critical - Denial of Service - SA-CORE-2019-009", + "link": "https://www.drupal.org/sa-core-2019-009", + "cve": null, + "affectedVersions": "8.1.0", + "source": "FriendsOfPHP/security-advisories", + "reportedAt": "2019-122222-18 00:00:00", + "composerRepository": "https://packagist.org", + "severity": "critical", + "sources": [ + { + "name": "FriendsOfPHP/security-advisories", + "remoteId": "drupal/core/2019-12-18-1.yaml" + }, + { + "name": "GitHub", + "remoteId": "GHSA-7v68-3pr5-h3cr" + } + ] + } + """); + + @Test + public void testDateTime() { + ComposerAdvisory vuln = ComposerAdvisoryParser.parseAdvisory(VULN_DRUPAL_INVALID_TIME); + Assert.assertNull(vuln.getReportedAt()); + } + + @Test + public void testSources() { + ComposerAdvisory vuln = ComposerAdvisoryParser.parseAdvisory(VULN_FOP); + Assert.assertEquals(2, vuln.getSources().size()); + Assert.assertTrue(vuln.getSources().containsKey("github")); + Assert.assertEquals("GHSA-r8v4-7vwj-983x", vuln.getSources().get("github")); + Assert.assertTrue(vuln.getSources().containsKey("friendsofphp/security-advisories")); + Assert.assertEquals("simplesamlphp/saml2/CVE-2016-9814.yaml", vuln.getSources().get("friendsofphp/security-advisories")); + } + + @Test + public void testParseNoErrors() throws IOException { + ComposerAdvisoryParser.parseAdvisory(VULN_DRUPAL); + ComposerAdvisoryParser.parseAdvisory(VULN_DRUPAL_INVALID_TIME); + ComposerAdvisoryParser.parseAdvisory(VULN_GHSA); + ComposerAdvisoryParser.parseAdvisory(VULN_FOP); + ComposerAdvisoryParser.parseAdvisory(VULN_FOP_CVE); + ComposerAdvisoryParser.parseAdvisory(VULN_FOP_NO_CVE); + ComposerAdvisoryParser.parseAdvisory(VULN_COMPOSER); + ComposerAdvisoryParser.parseAdvisory(VULN_WILDCARD_ALL); + ComposerAdvisoryParser.parseAdvisory(VULN_EXACT_VERSION); + } + +} diff --git a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java index 75463de7f8..7766508d2b 100644 --- a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java +++ b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java @@ -102,7 +102,7 @@ public void testLoadDefaultRepositories() throws Exception { Method method = generator.getClass().getDeclaredMethod("loadDefaultRepositories"); method.setAccessible(true); method.invoke(generator); - Assert.assertEquals(17, qm.getAllRepositories().size()); + Assert.assertEquals(18, qm.getAllRepositories().size()); } @Test diff --git a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java index bf62d0bc44..71500ff8b9 100644 --- a/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java @@ -28,6 +28,7 @@ import org.dependencytrack.persistence.DefaultObjectGenerator; import org.dependencytrack.persistence.QueryManager; import org.glassfish.jersey.server.ResourceConfig; +import org.json.JSONObject; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; @@ -62,16 +63,18 @@ public void getRepositoriesTest() { .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(17), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(18), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(17, json.size()); + Assert.assertEquals(18, json.size()); for (int i = 0; i < json.size(); i++) { Assert.assertNotNull(json.getJsonObject(i).getString("type")); Assert.assertNotNull(json.getJsonObject(i).getString("identifier")); Assert.assertNotNull(json.getJsonObject(i).getString("url")); Assert.assertTrue(json.getJsonObject(i).getInt("resolutionOrder") > 0); - Assert.assertTrue(json.getJsonObject(i).getBoolean("enabled")); + if (!json.getJsonObject(i).getString("identifier").equals("drupal8")) { + Assert.assertTrue(json.getJsonObject(i).getBoolean("enabled")); + } } } @@ -171,7 +174,6 @@ public void getRepositoryMetaUntrackedComponentTest() { Assert.assertEquals("The repository metadata for the specified component cannot be found.", body); } - @Test public void createRepositoryTest() { Repository repository = new Repository(); @@ -187,20 +189,19 @@ public void createRepositoryTest() { .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus()); - response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(18), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(19), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(18, json.size()); - Assert.assertEquals("MAVEN", json.getJsonObject(13).getString("type")); - Assert.assertEquals("test", json.getJsonObject(13).getString("identifier")); - Assert.assertEquals("www.foobar.com", json.getJsonObject(13).getString("url")); - Assert.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0); - Assert.assertTrue(json.getJsonObject(13).getBoolean("authenticationRequired")); - Assert.assertEquals("testuser", json.getJsonObject(13).getString("username")); - Assert.assertTrue(json.getJsonObject(13).getBoolean("enabled")); + Assert.assertEquals(19, json.size()); + Assert.assertEquals("MAVEN", json.getJsonObject(14).getString("type")); + Assert.assertEquals("test", json.getJsonObject(14).getString("identifier")); + Assert.assertEquals("www.foobar.com", json.getJsonObject(14).getString("url")); + Assert.assertTrue(json.getJsonObject(14).getInt("resolutionOrder") > 0); + Assert.assertTrue(json.getJsonObject(14).getBoolean("authenticationRequired")); + Assert.assertEquals("testuser", json.getJsonObject(14).getString("username")); + Assert.assertTrue(json.getJsonObject(14).getBoolean("enabled")); } @Test @@ -219,21 +220,20 @@ public void createNonInternalRepositoryTest() { .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); Assert.assertEquals(201, response.getStatus()); - response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(18), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(19), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(18, json.size()); - Assert.assertEquals("MAVEN", json.getJsonObject(13).getString("type")); - Assert.assertEquals("test", json.getJsonObject(13).getString("identifier")); - Assert.assertEquals("www.foobar.com", json.getJsonObject(13).getString("url")); - Assert.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0); - Assert.assertTrue(json.getJsonObject(13).getBoolean("authenticationRequired")); - Assert.assertFalse(json.getJsonObject(13).getBoolean("internal")); - Assert.assertEquals("testuser", json.getJsonObject(13).getString("username")); - Assert.assertTrue(json.getJsonObject(13).getBoolean("enabled")); + Assert.assertEquals(19, json.size()); + Assert.assertEquals("MAVEN", json.getJsonObject(14).getString("type")); + Assert.assertEquals("test", json.getJsonObject(14).getString("identifier")); + Assert.assertEquals("www.foobar.com", json.getJsonObject(14).getString("url")); + Assert.assertTrue(json.getJsonObject(14).getInt("resolutionOrder") > 0); + Assert.assertTrue(json.getJsonObject(14).getBoolean("authenticationRequired")); + Assert.assertFalse(json.getJsonObject(14).getBoolean("internal")); + Assert.assertEquals("testuser", json.getJsonObject(14).getString("username")); + Assert.assertTrue(json.getJsonObject(14).getBoolean("enabled")); } @Test @@ -243,6 +243,7 @@ public void createRepositoryAuthFalseTest() { repository.setEnabled(true); repository.setInternal(true); repository.setIdentifier("test"); + repository.setDescription("description"); repository.setUrl("www.foobar.com"); repository.setType(RepositoryType.MAVEN); Response response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) @@ -252,16 +253,17 @@ public void createRepositoryAuthFalseTest() { response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey).get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(18), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(19), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(18, json.size()); - Assert.assertEquals("MAVEN", json.getJsonObject(13).getString("type")); - Assert.assertEquals("test", json.getJsonObject(13).getString("identifier")); - Assert.assertEquals("www.foobar.com", json.getJsonObject(13).getString("url")); - Assert.assertTrue(json.getJsonObject(13).getInt("resolutionOrder") > 0); - Assert.assertFalse(json.getJsonObject(13).getBoolean("authenticationRequired")); - Assert.assertTrue(json.getJsonObject(13).getBoolean("enabled")); + Assert.assertEquals(19, json.size()); + Assert.assertEquals("MAVEN", json.getJsonObject(14).getString("type")); + Assert.assertEquals("test", json.getJsonObject(14).getString("identifier")); + Assert.assertEquals("description", json.getJsonObject(14).getString("description")); + Assert.assertEquals("www.foobar.com", json.getJsonObject(14).getString("url")); + Assert.assertTrue(json.getJsonObject(14).getInt("resolutionOrder") > 0); + Assert.assertFalse(json.getJsonObject(14).getBoolean("authenticationRequired")); + Assert.assertTrue(json.getJsonObject(14).getBoolean("enabled")); } @@ -274,6 +276,7 @@ public void updateRepositoryTest() throws Exception { repository.setPassword("testPassword"); repository.setInternal(true); repository.setIdentifier("test"); + repository.setDescription("description"); repository.setUrl("www.foobar.com"); repository.setType(RepositoryType.MAVEN); Response response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) @@ -284,6 +287,7 @@ public void updateRepositoryTest() throws Exception { for (Repository repository1 : repositoryList) { if (repository1.getIdentifier().equals("test")) { repository1.setAuthenticationRequired(false); + repository1.setDescription("new description"); response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) .post(Entity.entity(repository1, MediaType.APPLICATION_JSON)); Assert.assertEquals(200, response.getStatus()); @@ -294,10 +298,55 @@ public void updateRepositoryTest() throws Exception { for (Repository repository1 : repositoryList) { if (repository1.getIdentifier().equals("test")) { Assert.assertEquals(false, repository1.isAuthenticationRequired()); + Assert.assertEquals("new description", repository1.getDescription()); + break; + } + } + } + + } + + @Test + public void updateRepositoryTestAdvisoryMirroring() throws Exception { + Repository repository = new Repository(); + repository.setAuthenticationRequired(true); + repository.setEnabled(true); + repository.setUsername("testuser"); + repository.setPassword("testPassword"); + repository.setInternal(true); + repository.setIdentifier("composer_repo"); + repository.setUrl("www.foobar.com"); + repository.setType(RepositoryType.COMPOSER); + Response response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + .put(Entity.entity(repository, MediaType.APPLICATION_JSON)); + Assert.assertEquals(201, response.getStatus()); + + try (QueryManager qm = new QueryManager()) { + List repositoryList = qm.getRepositories(RepositoryType.COMPOSER).getList(Repository.class); + for (Repository repository1 : repositoryList) { + if (repository1.getIdentifier().equals("composer_repo")) { + Assert.assertNull(repository1.getConfig()); + repository1.setConfig("{\"advisoryMirroringEnabled\": true, \"advisoryAliasSyncEnabled\": true}"); + response = jersey.target(V1_REPOSITORY).request().header(X_API_KEY, apiKey) + .post(Entity.entity(repository1, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus()); + break; + } + } + + repositoryList = qm.getRepositories(RepositoryType.COMPOSER).getList(Repository.class); + for (Repository repository1 : repositoryList) { + if (repository1.getIdentifier().equals("composer_repo")) { + Assert.assertNotNull(repository1.getConfig()); + JSONObject jsonConfig = new JSONObject(repository1.getConfig()); + Assert.assertTrue(jsonConfig.optBoolean("advisoryMirroringEnabled")); + Assert.assertTrue(jsonConfig.optBoolean("advisoryAliasSyncEnabled")); break; } } } } + + } diff --git a/src/test/java/org/dependencytrack/tasks/RepoMetaAnalysisTaskTest.java b/src/test/java/org/dependencytrack/tasks/RepoMetaAnalysisTaskTest.java index 02455e160b..995b48e023 100644 --- a/src/test/java/org/dependencytrack/tasks/RepoMetaAnalysisTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/RepoMetaAnalysisTaskTest.java @@ -70,7 +70,7 @@ public void informTestNullPassword() throws Exception { 20210213164433 - + """.getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON)) ) .withHeader("X-CheckSum-MD5", "md5hash") @@ -85,7 +85,7 @@ public void informTestNullPassword() throws Exception { component.setName("junit"); component.setPurl(new PackageURL("pkg:maven/junit/junit@4.12")); qm.createComponent(component, false); - qm.createRepository(RepositoryType.MAVEN, "test", wireMockRule.baseUrl(), true, false, true, "testuser", null); + qm.createRepository(RepositoryType.MAVEN, "test", null, wireMockRule.baseUrl(), true, false, true, "testuser", null, null); new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component))); RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "junit", "junit"); qm.getPersistenceManager().refresh(metaComponent); @@ -116,7 +116,7 @@ public void informTestNullUserName() throws Exception { 20210213164433 - + """.getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON)) ) .withHeader("X-CheckSum-MD5", "md5hash") @@ -131,7 +131,7 @@ public void informTestNullUserName() throws Exception { component.setName("test1"); component.setPurl(new PackageURL("pkg:maven/test1/test1@1.2.0")); qm.createComponent(component, false); - qm.createRepository(RepositoryType.MAVEN, "test", wireMockRule.baseUrl(), true, false, true, null, "testPassword"); + qm.createRepository(RepositoryType.MAVEN, "test", null, wireMockRule.baseUrl(), true, false, true, null, "testPassword", null); new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component))); RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "test1", "test1"); qm.getPersistenceManager().refresh(metaComponent); @@ -162,7 +162,7 @@ public void informTestNullUserNameAndPassword() throws Exception { 20210213164433 - + """.getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON)) ) .withHeader("X-CheckSum-MD5", "md5hash") @@ -177,7 +177,7 @@ public void informTestNullUserNameAndPassword() throws Exception { component.setName("junit"); component.setPurl(new PackageURL("pkg:maven/test2/test2@4.12")); qm.createComponent(component, false); - qm.createRepository(RepositoryType.MAVEN, "test", wireMockRule.baseUrl(), true, false, false, null, null); + qm.createRepository(RepositoryType.MAVEN, "test", null, wireMockRule.baseUrl(), true, false, false, null, null, null); new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component))); RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "test2", "test2"); qm.getPersistenceManager().refresh(metaComponent); @@ -208,7 +208,7 @@ public void informTestUserNameAndPassword() throws Exception { 20210213164433 - + """.getBytes(), new ContentTypeHeader(MediaType.APPLICATION_JSON)) ) .withHeader("X-CheckSum-MD5", "md5hash") @@ -223,7 +223,7 @@ public void informTestUserNameAndPassword() throws Exception { component.setName("test3"); component.setPurl(new PackageURL("pkg:maven/test3/test3@4.12")); qm.createComponent(component, false); - qm.createRepository(RepositoryType.MAVEN, "test", wireMockRule.baseUrl(), true, false, true, "testUser", "testPassword"); + qm.createRepository(RepositoryType.MAVEN, "test", null, wireMockRule.baseUrl(), true, false, true, "testUser", "testPassword", null); new RepositoryMetaAnalyzerTask().inform(new RepositoryMetaEvent(List.of(component))); RepositoryMetaComponent metaComponent = qm.getRepositoryMetaComponent(RepositoryType.MAVEN, "test3", "test3"); qm.getPersistenceManager().refresh(metaComponent); diff --git a/src/test/java/org/dependencytrack/tasks/repositories/ComposerAdvisoryMirrorTaskTest.java b/src/test/java/org/dependencytrack/tasks/repositories/ComposerAdvisoryMirrorTaskTest.java new file mode 100644 index 0000000000..dd1b250946 --- /dev/null +++ b/src/test/java/org/dependencytrack/tasks/repositories/ComposerAdvisoryMirrorTaskTest.java @@ -0,0 +1,868 @@ +/* + * Copyright 2022 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.dependencytrack.tasks.repositories; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_ENABLED; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +import java.io.File; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHeaders; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.AffectedVersionAttribution; +import org.dependencytrack.model.Repository; +import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityAlias; +import org.dependencytrack.model.VulnerableSoftware; +import org.dependencytrack.parser.composer.ComposerAdvisoryParser; +import org.dependencytrack.parser.composer.ComposerAdvisoryParserTest; +import org.dependencytrack.parser.composer.model.ComposerAdvisory; +import org.json.JSONObject; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockserver.client.MockServerClient; +import org.mockserver.integration.ClientAndServer; + +import com.github.packageurl.PackageURL; + +import alpine.model.IConfigProperty; + + +public class ComposerAdvisoryMirrorTaskTest extends PersistenceCapableTest { + + private static ClientAndServer mockServer; + + private static final String CONFIG_MIRROR_ENABLED_WITH_ALIAS = "{\"advisoryMirroringEnabled\": true, \"advisoryAliasSyncEnabled\": true}"; + + @Before + public void setUp() { + qm.createConfigProperty(VULNERABILITY_SOURCE_NVD_ENABLED.getGroupName(), + VULNERABILITY_SOURCE_NVD_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + ""); + qm.createConfigProperty(VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ENABLED.getGroupName(), + VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + ""); + + mockServer.reset(); + } + + @BeforeClass + public static void beforeClass() { + mockServer = ClientAndServer.startClientAndServer(1080); + } + + @AfterClass + public static void afterClass() { + mockServer.stop(); + } + + @Test + public void testTruncateSummaryAndAffectedVersions() { + String longTitle = "In uvc_scan_chain_forward of uvc_driver.c, there is a possible linked list corruption due to an unusual root cause. This could lead to local escalation of privilege in the kernel with no additional execution privileges needed. User interaction is not needed for exploitation."; + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + ComposerAdvisory composerAdvisory = ComposerAdvisoryParser + .parseAdvisory(ComposerAdvisoryParserTest.VULN_FOP); + composerAdvisory.setTitle(longTitle); + Vulnerability vuln = task.mapComposerAdvisoryToVulnerability(composerAdvisory, true); + Assert.assertEquals(vuln.getTitle(), StringUtils.abbreviate(longTitle, "...", 255)); + + String longAffected = "\\u003E=8.0.0,\\u003C8.1.0|\\u003E=8.1.0,\\u003C8.2.0|\\u003E=8.2.0,\\u003C8.3.0|\\u003E=8.3.0,\\u003C8.4.0|\\u003E=8.4.0,\\u003C8.5.0|\\u003E=8.5.0,\\u003C8.6.0|\\u003E=8.6.0,\\u003C8.7.0|\\u003E=8.7.0,\\u003C8.8.0|\\u003E=8.8.0,\\u003C8.9.0|\\u003E=8.9.0,\\u003C9.0.0|\\u003E=9.0.0,\\u003C9.1.0|\\u003E=9.1.0,\\u003C9.2.0|\\u003E=9.2.0,\\u003C9.3.0|\\u003E=9.3.0,\\u003C9.4.0|\\u003E=9.4.0,\\u003C9.5.0|\\u003E=9.5.0,\\u003C10.0.0|\\u003E=10.0.0,\\u003C10.1.0|\\u003E=10.1.0,\\u003C10.1.8|\\u003E=10.2.0,\\u003C10.2.2"; + composerAdvisory = ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_FOP); + composerAdvisory.setAffectedVersionsCve(longAffected); + vuln = task.mapComposerAdvisoryToVulnerability(composerAdvisory, true); + Assert.assertEquals(vuln.getVulnerableVersions(), StringUtils.abbreviate(longAffected, "...", 255)); + } + + @Test + public void testextractVulnIdDrupal() { + Vulnerability.Source source1 = Vulnerability.Source.resolve(ComposerAdvisoryMirrorTask + .extractVulnId(ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_DRUPAL))); + Assert.assertEquals(Vulnerability.Source.DRUPAL, source1); + } + + @Test + public void testextractVulnIdFriendsOfPhp() { + Vulnerability.Source source2 = Vulnerability.Source.resolve(ComposerAdvisoryMirrorTask + .extractVulnId(ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_FOP))); + Assert.assertEquals(Vulnerability.Source.GITHUB, source2); + } + + @Test + public void testextractVulnIdGHSA() { + Vulnerability.Source source3 = Vulnerability.Source.resolve(ComposerAdvisoryMirrorTask + .extractVulnId(ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_GHSA))); + Assert.assertEquals(Vulnerability.Source.GITHUB, source3); + } + + @Test + public void testextractVulnIdFriendsOfPhpCVE() { + Vulnerability.Source source4 = Vulnerability.Source.resolve(ComposerAdvisoryMirrorTask + .extractVulnId(ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_FOP_CVE))); + Assert.assertEquals(Vulnerability.Source.NVD, source4); + } + + @Test + public void testextractVulnIdFriendsOfPhpNoCVE() { + Vulnerability.Source source4 = Vulnerability.Source.resolve(ComposerAdvisoryMirrorTask + .extractVulnId(ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_FOP_NO_CVE))); + Assert.assertEquals(Vulnerability.Source.GITHUB, source4); + } + + @Test + public void testextractVulnIdComposer() { + Vulnerability.Source source5 = Vulnerability.Source.resolve(ComposerAdvisoryMirrorTask + .extractVulnId(ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_COMPOSER))); + Assert.assertEquals(Vulnerability.Source.COMPOSER, source5); + } + + @Test + public void testDrupalAffectedVersionMapping() throws IOException { + ComposerAdvisory vuln = ComposerAdvisoryParser + .parseAdvisory(ComposerAdvisoryParserTest.VULN_DRUPAL); + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + List mapVulnerabilityToVulnerableSoftware = task.mapVulnerabilityToVulnerableSoftware(qm, + vuln); + Assert.assertEquals(2, mapVulnerabilityToVulnerableSoftware.size()); + assertThat(mapVulnerabilityToVulnerableSoftware).satisfiesExactlyInAnyOrder( + range -> { + assertThat(range.getVersionStartIncluding()).isEqualTo("8.0.0"); + assertThat(range.getVersionEndExcluding()).isEqualTo("8.4.7"); + }, + range -> { + assertThat(range.getVersionStartIncluding()).isEqualTo("8.5.0"); + assertThat(range.getVersionEndExcluding()).isEqualTo("8.5.2"); + }); + } + + @Test + public void testPackagistAffectedVersionMapping() throws IOException { + ComposerAdvisory vuln = ComposerAdvisoryParser + .parseAdvisory(ComposerAdvisoryParserTest.VULN_FOP); + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + List mapVulnerabilityToVulnerableSoftware = task.mapVulnerabilityToVulnerableSoftware(qm, + vuln); + Assert.assertEquals(4, mapVulnerabilityToVulnerableSoftware.size()); +// "affectedVersions": "\u003C1.8.1|\u003E=1.9.0,\u003C1.9.1|\u003E=1.10,\u003C1.10.3|\u003E=2.0,\u003C2.3.3", + assertThat(mapVulnerabilityToVulnerableSoftware).satisfiesExactlyInAnyOrder( + range -> { + assertThat(range.getVersionStartIncluding()).isNull(); + assertThat(range.getVersionStartExcluding()).isNull(); + assertThat(range.getVersionEndIncluding()).isNull(); + assertThat(range.getVersionEndExcluding()).isEqualTo("1.8.1"); + }, + range -> { + assertThat(range.getVersionStartIncluding()).isEqualTo("1.9.0"); + assertThat(range.getVersionStartExcluding()).isNull(); + assertThat(range.getVersionEndIncluding()).isNull(); + assertThat(range.getVersionEndExcluding()).isEqualTo("1.9.1"); + }, + range -> { + assertThat(range.getVersionStartIncluding()).isEqualTo("1.10"); + assertThat(range.getVersionStartExcluding()).isNull(); + assertThat(range.getVersionEndIncluding()).isNull(); + assertThat(range.getVersionEndExcluding()).isEqualTo("1.10.3"); + }, + range -> { + assertThat(range.getVersionStartIncluding()).isEqualTo("2.0"); + assertThat(range.getVersionStartExcluding()).isNull(); + assertThat(range.getVersionEndIncluding()).isNull(); + assertThat(range.getVersionEndExcluding()).isEqualTo("2.3.3"); + }); + } + + @Test + public void testDrupalWildcardAffectedVersionMapping() throws IOException { + ComposerAdvisory vuln = ComposerAdvisoryParser + .parseAdvisory(ComposerAdvisoryParserTest.VULN_WILDCARD_ALL); + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + List mapVulnerabilityToVulnerableSoftware = task.mapVulnerabilityToVulnerableSoftware(qm, + vuln); + Assert.assertEquals(1, mapVulnerabilityToVulnerableSoftware.size()); + assertThat(mapVulnerabilityToVulnerableSoftware).satisfiesExactlyInAnyOrder( + range -> { + assertThat(range.getVersionStartIncluding()).isNull(); + assertThat(range.getVersionStartExcluding()).isNull(); + assertThat(range.getVersionEndIncluding()).isNull(); + assertThat(range.getVersionEndExcluding()).isEqualTo("999.999.999"); + }); + } + + @Test + public void testDrupalExactVersionMapping() throws IOException { + ComposerAdvisory vuln = ComposerAdvisoryParser + .parseAdvisory(ComposerAdvisoryParserTest.VULN_EXACT_VERSION); + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + List mapVulnerabilityToVulnerableSoftware = task.mapVulnerabilityToVulnerableSoftware(qm, + vuln); + Assert.assertEquals(1, mapVulnerabilityToVulnerableSoftware.size()); + assertThat(mapVulnerabilityToVulnerableSoftware).satisfiesExactlyInAnyOrder( + range -> { + assertThat(range.getVersionStartIncluding()).isEqualTo("8.1.0"); + assertThat(range.getVersionStartExcluding()).isNull(); + assertThat(range.getVersionEndIncluding()).isEqualTo("8.1.0"); + assertThat(range.getVersionEndExcluding()).isNull(); + }); + } + + @Test + public void testDrupalAdvisory() throws Exception { + doDrupalAdvisory(true); + } + + @Test + public void testDrupalAdvisorySkipAliases() throws Exception { + doDrupalAdvisory(false); + } + + public void doDrupalAdvisory(boolean aliasSync) throws Exception { + ComposerAdvisory advisory = ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_DRUPAL); + Assert.assertNotNull(advisory); + + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + + task.processAdvisory(qm, advisory, aliasSync); + + final Consumer assertVulnerability = (vulnerability) -> { + Assert.assertNotNull(vulnerability); + Assert.assertEquals("SA-CORE-2018-003", vulnerability.getVulnId()); + Assert.assertEquals("DRUPAL", vulnerability.getSource()); + Assert.assertEquals(">= 8.0.0 <8.4.7 || >=8.5.0 <8.5.2", vulnerability.getVulnerableVersions()); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getTitle())); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getDescription())); + Assert.assertEquals(Severity.UNASSIGNED, vulnerability.getSeverity()); + Assert.assertNull(vulnerability.getCreated()); + Assert.assertNotNull(vulnerability.getPublished()); + Assert.assertEquals(LocalDateTime.of(2018, 4, 18, 15, 34, 9).toInstant(ZoneOffset.UTC), + vulnerability.getPublished().toInstant()); + Assert.assertNotNull(vulnerability.getUpdated()); + Assert.assertEquals(LocalDateTime.of(2018, 4, 18, 15, 34, 9).toInstant(ZoneOffset.UTC), + vulnerability.getUpdated().toInstant()); + }; + + Vulnerability vulnerability = qm.getVulnerabilityByVulnId("DRUPAL", "SA-CORE-2018-003", true); + assertVulnerability.accept(vulnerability); + + List vulnerableSoftware = qm.getAllVulnerableSoftwareByPurl( + new PackageURL("pkg:composer/drupal/core")); + Assert.assertEquals(2, vulnerableSoftware.size()); + Assert.assertEquals("8.0.0", vulnerableSoftware.get(0).getVersionStartIncluding()); + Assert.assertEquals("8.4.7", vulnerableSoftware.get(0).getVersionEndExcluding()); + Assert.assertEquals("8.5.0", vulnerableSoftware.get(1).getVersionStartIncluding()); + Assert.assertEquals("8.5.2", vulnerableSoftware.get(1).getVersionEndExcluding()); + + final List aliases = qm.getVulnerabilityAliases(vulnerability); + Assert.assertEquals(aliasSync ? 1 : 0, aliases.size()); + if (aliasSync) { + assertThat(aliases).satisfiesExactly( + alias -> { + assertNull(alias.getComposerId()); + assertEquals("CVE-2018-9861", alias.getCveId()); + assertNull(alias.getGhsaId()); + assertEquals("SA-CORE-2018-003", alias.getDrupalId()); + assertNull(alias.getGsdId()); + assertNull(alias.getInternalId()); + assertNull(alias.getOsvId()); + assertNull(alias.getSnykId()); + assertNull(alias.getSonatypeId()); + assertNull(alias.getVulnDbId()); + }); + } + } + + @Test + public void testFop() throws Exception { + doFop(true); + } + + @Test + public void testFopSkipAliases() throws Exception { + doFop(false); + } + + public void doFop(boolean aliasSync) throws Exception { + ComposerAdvisory advisory = ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_FOP); + Assert.assertNotNull(advisory); + + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + + task.processAdvisory(qm, advisory, aliasSync); + + final Consumer assertVulnerability = (vulnerability) -> { + Assert.assertNotNull(vulnerability); + Assert.assertEquals("GHSA-r8v4-7vwj-983x", vulnerability.getVulnId()); + Assert.assertEquals("GITHUB", vulnerability.getSource()); + Assert.assertEquals("<1.8.1|>=1.9.0,<1.9.1|>=1.10,<1.10.3|>=2.0,<2.3.3", + vulnerability.getVulnerableVersions()); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getTitle())); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getDescription())); + Assert.assertEquals(Severity.CRITICAL, vulnerability.getSeverity()); + Assert.assertNull(vulnerability.getCreated()); + Assert.assertNotNull(vulnerability.getPublished()); + Assert.assertEquals(LocalDateTime.of(2016, 11, 29, 13, 12, 44).toInstant(ZoneOffset.UTC), + vulnerability.getPublished().toInstant()); + Assert.assertNotNull(vulnerability.getUpdated()); + Assert.assertEquals(LocalDateTime.of(2016, 11, 29, 13, 12, 44).toInstant(ZoneOffset.UTC), + vulnerability.getUpdated().toInstant()); + }; + + Vulnerability vulnerability = qm.getVulnerabilityByVulnId(Vulnerability.Source.GITHUB, "GHSA-r8v4-7vwj-983x", true); + assertVulnerability.accept(vulnerability); + + List vulnerableSoftware = qm.getAllVulnerableSoftwareByPurl( + new PackageURL("pkg:composer/simplesamlphp/saml2")); + + Assert.assertEquals(4, vulnerableSoftware.size()); + Assert.assertEquals("1.8.1", vulnerableSoftware.get(0).getVersionEndExcluding()); + Assert.assertEquals("1.9.0", vulnerableSoftware.get(1).getVersionStartIncluding()); + Assert.assertEquals("1.9.1", vulnerableSoftware.get(1).getVersionEndExcluding()); + //Is this ok, or should it become 1.10.0? + Assert.assertEquals("1.10", vulnerableSoftware.get(2).getVersionStartIncluding()); + Assert.assertEquals("1.10.3", vulnerableSoftware.get(2).getVersionEndExcluding()); + Assert.assertEquals("2.0", vulnerableSoftware.get(3).getVersionStartIncluding()); + Assert.assertEquals("2.3.3", vulnerableSoftware.get(3).getVersionEndExcluding()); + + final List aliases = qm.getVulnerabilityAliases(vulnerability); + Assert.assertEquals(aliasSync ? 1 : 0, aliases.size()); + if (aliasSync) { + assertThat(aliases).satisfiesExactly( + alias -> { + assertNull(alias.getComposerId()); + assertEquals("CVE-2016-9814", alias.getCveId()); + assertEquals("GHSA-r8v4-7vwj-983x", alias.getGhsaId()); + assertNull(alias.getDrupalId()); + assertNull(alias.getGsdId()); + assertNull(alias.getInternalId()); + assertNull(alias.getOsvId()); + assertNull(alias.getSnykId()); + assertNull(alias.getSonatypeId()); + assertNull(alias.getVulnDbId()); + }); + } + } + + @Test + public void testFopCve() throws Exception { + doFop(true); + } + + @Test + public void testFopCveSkipAliases() throws Exception { + doFop(false); + } + + public void doFopCve(boolean aliasSync) throws Exception { + ComposerAdvisory advisory = ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_FOP_CVE); + Assert.assertNotNull(advisory); + + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + + task.processAdvisory(qm, advisory, aliasSync); + + final Consumer assertVulnerability = (vulnerability) -> { + Assert.assertNotNull(vulnerability); + Assert.assertEquals("CVE-2016-9814", vulnerability.getVulnId()); + Assert.assertEquals("NVD", vulnerability.getSource()); + Assert.assertEquals("<1.8.1|>=1.9.0,<1.9.1|>=1.10,<1.10.3|>=2.0,<2.3.3", + vulnerability.getVulnerableVersions()); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getTitle())); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getDescription())); + Assert.assertEquals(Severity.CRITICAL, vulnerability.getSeverity()); + Assert.assertNull(vulnerability.getCreated()); + Assert.assertNotNull(vulnerability.getPublished()); + Assert.assertEquals(LocalDateTime.of(2016, 11, 29, 13, 12, 44).toInstant(ZoneOffset.UTC), + vulnerability.getPublished().toInstant()); + Assert.assertNotNull(vulnerability.getUpdated()); + Assert.assertEquals(LocalDateTime.of(2016, 11, 29, 13, 12, 44).toInstant(ZoneOffset.UTC), + vulnerability.getUpdated().toInstant()); + }; + + Vulnerability vulnerability = qm.getVulnerabilityByVulnId(Vulnerability.Source.NVD, "CVE-2016-9814", true); + assertVulnerability.accept(vulnerability); + + List vulnerableSoftware = qm.getAllVulnerableSoftwareByPurl( + new PackageURL("pkg:composer/simplesamlphp/saml2")); + + Assert.assertEquals(4, vulnerableSoftware.size()); + Assert.assertEquals("1.8.1", vulnerableSoftware.get(0).getVersionEndExcluding()); + Assert.assertEquals("1.9.0", vulnerableSoftware.get(1).getVersionStartIncluding()); + Assert.assertEquals("1.9.1", vulnerableSoftware.get(1).getVersionEndExcluding()); + //Is this ok, or should it become 1.10.0? + Assert.assertEquals("1.10", vulnerableSoftware.get(2).getVersionStartIncluding()); + Assert.assertEquals("1.10.3", vulnerableSoftware.get(2).getVersionEndExcluding()); + Assert.assertEquals("2.0", vulnerableSoftware.get(3).getVersionStartIncluding()); + Assert.assertEquals("2.3.3", vulnerableSoftware.get(3).getVersionEndExcluding()); + + final List aliases = qm.getVulnerabilityAliases(vulnerability); + Assert.assertEquals(aliasSync ? 1 : 0, aliases.size()); + if (aliasSync) { + assertThat(aliases).satisfiesExactly( + alias -> { + assertNull(alias.getComposerId()); + assertEquals("CVE-2016-9814", alias.getCveId()); + assertNull(alias.getDrupalId()); + assertNull(alias.getGsdId()); + assertNull(alias.getInternalId()); + assertNull(alias.getOsvId()); + assertNull(alias.getSnykId()); + assertNull(alias.getSonatypeId()); + assertNull(alias.getVulnDbId()); + }); + } + } + + + @Test + public void testFopNoCve() throws Exception { + doFopNoCve(true); + } + + @Test + public void testFopNoCveSkipAliases() throws Exception { + doFopNoCve(false); + } + + public void doFopNoCve(boolean aliasSync) throws Exception { + ComposerAdvisory advisory = ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_FOP_NO_CVE); + Assert.assertNotNull(advisory); + + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + + task.processAdvisory(qm, advisory, aliasSync); + + final Consumer assertVulnerability = (vulnerability) -> { + Assert.assertNotNull(vulnerability); + Assert.assertEquals("GHSA-7v68-3pr5-h3cr", vulnerability.getVulnId()); + Assert.assertEquals("GITHUB", vulnerability.getSource()); + Assert.assertEquals(">=8.0.0,<8.1.0|>=8.1.0,<8.2.0|>=8.2.0,<8.3.0|>=8.3.0,<8.4.0|>=8.4.0,<8.5.0|>=8.5.0,<8.6.0|>=8.6.0,<8.7.0|>=8.7.0,<8.7.11|>=8.8.0,<8.8.1", + vulnerability.getVulnerableVersions()); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getTitle())); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getDescription())); + Assert.assertEquals(Severity.CRITICAL, vulnerability.getSeverity()); + Assert.assertNull(vulnerability.getCreated()); + Assert.assertNotNull(vulnerability.getPublished()); + Assert.assertEquals(LocalDateTime.of(2019, 12, 18, 0, 0, 0).toInstant(ZoneOffset.UTC), + vulnerability.getPublished().toInstant()); + Assert.assertNotNull(vulnerability.getUpdated()); + Assert.assertEquals(LocalDateTime.of(2019, 12, 18, 0, 0, 0).toInstant(ZoneOffset.UTC), + vulnerability.getUpdated().toInstant()); + }; + + Vulnerability vulnerability = qm.getVulnerabilityByVulnId(Vulnerability.Source.GITHUB, "GHSA-7v68-3pr5-h3cr", true); + assertVulnerability.accept(vulnerability); + + List vulnerableSoftware = qm.getAllVulnerableSoftwareByPurl( + new PackageURL("pkg:composer/drupal/core")); + + Assert.assertEquals(9, vulnerableSoftware.size()); + Assert.assertEquals("8.0.0", vulnerableSoftware.get(0).getVersionStartIncluding()); + Assert.assertEquals("8.1.0", vulnerableSoftware.get(0).getVersionEndExcluding()); + Assert.assertEquals("8.1.0", vulnerableSoftware.get(1).getVersionStartIncluding()); + Assert.assertEquals("8.2.0", vulnerableSoftware.get(1).getVersionEndExcluding()); + //Is this ok, or should it become 1.10.0? + Assert.assertEquals("8.2.0", vulnerableSoftware.get(2).getVersionStartIncluding()); + Assert.assertEquals("8.3.0", vulnerableSoftware.get(2).getVersionEndExcluding()); + Assert.assertEquals("8.3.0", vulnerableSoftware.get(3).getVersionStartIncluding()); + Assert.assertEquals("8.4.0", vulnerableSoftware.get(3).getVersionEndExcluding()); + + Assert.assertEquals("8.4.0", vulnerableSoftware.get(4).getVersionStartIncluding()); + Assert.assertEquals("8.5.0", vulnerableSoftware.get(4).getVersionEndExcluding()); + Assert.assertEquals("8.5.0", vulnerableSoftware.get(5).getVersionStartIncluding()); + Assert.assertEquals("8.6.0", vulnerableSoftware.get(5).getVersionEndExcluding()); + Assert.assertEquals("8.6.0", vulnerableSoftware.get(6).getVersionStartIncluding()); + Assert.assertEquals("8.7.0", vulnerableSoftware.get(6).getVersionEndExcluding()); + Assert.assertEquals("8.7.0", vulnerableSoftware.get(7).getVersionStartIncluding()); + Assert.assertEquals("8.7.11", vulnerableSoftware.get(7).getVersionEndExcluding()); + Assert.assertEquals("8.8.0", vulnerableSoftware.get(8).getVersionStartIncluding()); + Assert.assertEquals("8.8.1", vulnerableSoftware.get(8).getVersionEndExcluding()); + + final List aliases = qm.getVulnerabilityAliases(vulnerability); + Assert.assertEquals(aliasSync ? 0 : 0, aliases.size()); + } + + @Test + public void testGHSAAdvisory() throws Exception { + doGHSAAdvisory(true); + } + + @Test + public void testGHSAAdvisorySkipAliases() throws Exception { + doGHSAAdvisory(false); + } + + public void doGHSAAdvisory(boolean aliasSync) throws Exception { + ComposerAdvisory advisory = ComposerAdvisoryParser.parseAdvisory(ComposerAdvisoryParserTest.VULN_GHSA); + Assert.assertNotNull(advisory); + + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + + task.processAdvisory(qm, advisory, aliasSync); + + final Consumer assertVulnerability = (vulnerability) -> { + Assert.assertNotNull(vulnerability); + Assert.assertEquals("GHSA-297f-r9w7-w492", vulnerability.getVulnId()); + Assert.assertEquals("GITHUB", vulnerability.getSource()); + Assert.assertEquals("=2.4.4|>=2.4.0,<2.4.3-p3|<2.3.7-p4", + vulnerability.getVulnerableVersions()); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getTitle())); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getDescription())); + Assert.assertEquals(Severity.HIGH, vulnerability.getSeverity()); + Assert.assertNull(vulnerability.getCreated()); + Assert.assertNotNull(vulnerability.getPublished()); + Assert.assertEquals(LocalDateTime.of(2022, 10, 20, 19, 00, 29).toInstant(ZoneOffset.UTC), + vulnerability.getPublished().toInstant()); + Assert.assertNotNull(vulnerability.getUpdated()); + Assert.assertEquals(LocalDateTime.of(2022, 10, 20, 19, 00, 29).toInstant(ZoneOffset.UTC), + vulnerability.getUpdated().toInstant()); + }; + + Vulnerability vulnerability = qm.getVulnerabilityByVulnId(Vulnerability.Source.GITHUB, "GHSA-297f-r9w7-w492", true); + assertVulnerability.accept(vulnerability); + + List vulnerableSoftware = qm.getAllVulnerableSoftwareByPurl( + new PackageURL("pkg:composer/magento/community-edition")); + Assert.assertEquals(3, vulnerableSoftware.size()); + Assert.assertEquals("2.4.4", vulnerableSoftware.get(0).getVersionStartIncluding()); + Assert.assertEquals("2.4.4", vulnerableSoftware.get(0).getVersionEndIncluding()); + Assert.assertEquals("2.4.0", vulnerableSoftware.get(1).getVersionStartIncluding()); + Assert.assertEquals("2.4.3-p3", vulnerableSoftware.get(1).getVersionEndExcluding()); + Assert.assertEquals("2.3.7-p4", vulnerableSoftware.get(2).getVersionEndExcluding()); + + final List aliases = qm.getVulnerabilityAliases(vulnerability); + Assert.assertEquals(aliasSync ? 1 : 0, aliases.size()); + if (aliasSync) { + assertThat(aliases).satisfiesExactly( + alias -> { + assertNull(alias.getComposerId()); + assertThat(alias.getCveId().equals("CVE-2022-42344")); + assertThat(alias.getGhsaId().equals("GHSA-r8v4-7vwj-983x")); + assertNull(alias.getDrupalId()); + assertNull(alias.getGsdId()); + assertNull(alias.getInternalId()); + assertNull(alias.getOsvId()); + assertNull(alias.getSnykId()); + assertNull(alias.getSonatypeId()); + assertNull(alias.getVulnDbId()); + }); + } + } + + private Repository setupPackagistAdvisoryMock() throws Exception { + final File packagistRepoRootFile = ComposerMetaAnalyzerTest.getRepoResourceFile("repo.packagist.org", "packages"); + final File advisoryFile = ComposerMetaAnalyzerTest.getRepoResourceFile("repo.packagist.org", "advisories"); + + @SuppressWarnings("resource") + MockServerClient mockClient = new MockServerClient("localhost", mockServer.getPort()); + String mockUrl = String.format("http://localhost:%d", mockServer.getPort()); + mockClient.when( + request() + .withMethod("GET") + .withPath("/packages.json")) + .respond( + response() + .withStatusCode(200) + .withHeader(HttpHeaders.CONTENT_TYPE, + "application/json") + .withBody(getRepoRootForMock(packagistRepoRootFile, mockUrl))); + + mockClient.when( + request() + .withMethod("GET") + .withPath("/api/security-advisories") + .withQueryStringParameter("updatedSince", "100") + ) + .respond( + response() + .withStatusCode(200) + .withHeader(HttpHeaders.CONTENT_TYPE, + "application/json") + .withBody(new String(ComposerMetaAnalyzerTest.getTestData(advisoryFile)))); + + return qm.createRepository(RepositoryType.COMPOSER, "packagist", null, mockUrl, true, false, false, null, null, CONFIG_MIRROR_ENABLED_WITH_ALIAS); + } + + @Test + public void testPackagistAdvisories() throws Exception { + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + + Repository repo = setupPackagistAdvisoryMock(); + + Assert.assertTrue(task.mirrorAdvisories(qm, repo)); + Assert.assertEquals(10, qm.getVulnerabilities().getTotal()); + + //Vulnerabilities should not have PKSA ids if other IDs are present + Assert.assertNull(qm.getVulnerabilityByVulnId(Vulnerability.Source.COMPOSER, "PKSA-q4rt-5vfc-wksb", true)); + Vulnerability vulnerability1 = qm.getVulnerabilityByVulnId(Vulnerability.Source.GITHUB, "GHSA-2697-96mv-3gfm", true); + + Assert.assertNotNull(vulnerability1); + Assert.assertEquals("GHSA-2697-96mv-3gfm", vulnerability1.getVulnId()); + Assert.assertEquals("CVE-2024-50701", vulnerability1.getAliases().get(0).getCveId()); + Assert.assertNull(vulnerability1.getAliases().get(0).getComposerId()); + + Assert.assertEquals("<3.1.3.1", vulnerability1.getVulnerableVersions()); + Assert.assertEquals(1, vulnerability1.getVulnerableSoftware().size()); + Assert.assertNull(vulnerability1.getVulnerableSoftware().get(0).getVersionStartIncluding()); + Assert.assertNull(vulnerability1.getVulnerableSoftware().get(0).getVersionStartExcluding()); + Assert.assertNull(vulnerability1.getVulnerableSoftware().get(0).getVersionEndIncluding()); + Assert.assertEquals("3.1.3.1", vulnerability1.getVulnerableSoftware().get(0).getVersionEndExcluding()); + + } + + @Test + public void testPackagistAdvisoriesExistingGHSA() throws Exception { + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + + var vs1 = new VulnerableSoftware(); + vs1.setPurlType("composer"); + vs1.setPurlNamespace("tltneon"); + vs1.setPurlName("lgsl"); + vs1.setVersionStartIncluding("2.13.0"); + vs1.setVersionEndIncluding("2.13.2.0"); + vs1.setVulnerable(true); + vs1 = qm.persist(vs1); + + var vs2 = new VulnerableSoftware(); + vs2.setPurlType("composer"); + vs2.setPurlNamespace("tltneon"); + vs2.setPurlName("lgsl"); + vs2.setVersionEndExcluding("7.0.0"); + vs2.setVulnerable(true); + vs2 = qm.persist(vs2); + + var existingVuln = new Vulnerability(); + existingVuln.setVulnId("GHSA-xx95-62h6-h7v3"); + existingVuln.setTitle("TITLE THAT SHOULD NOT GET OVERWRITTEN"); + existingVuln.setSource(Vulnerability.Source.GITHUB); + existingVuln.setVulnerableSoftware(List.of(vs1, vs2)); + existingVuln = qm.createVulnerability(existingVuln, false); + qm.updateAffectedVersionAttribution(existingVuln, vs1, Vulnerability.Source.GITHUB); + qm.updateAffectedVersionAttribution(existingVuln, vs2, Vulnerability.Source.GITHUB); + + Repository repo = setupPackagistAdvisoryMock(); + + Assert.assertTrue(task.mirrorAdvisories(qm, repo)); + Assert.assertEquals(10, qm.getVulnerabilities().getTotal()); + + Vulnerability vulnerability1 = qm.getVulnerabilityByVulnId(Vulnerability.Source.GITHUB, "GHSA-xx95-62h6-h7v3", true); + + Assert.assertNotNull(vulnerability1); + + Assert.assertEquals(existingVuln.getTitle(), vulnerability1.getTitle()); + + final List vsList = vulnerability1.getVulnerableSoftware(); + assertThat(vsList).satisfiesExactlyInAnyOrder( + // The version range that was reported by another source must be retained. + // There must be no attribution to OSV for this range. + vs -> { + assertThat(vs.getPurlType()).isEqualTo("composer"); + assertThat(vs.getPurlNamespace()).isEqualTo("tltneon"); + assertThat(vs.getPurlName()).isEqualTo("lgsl"); + assertThat(vs.getPurlVersion()).isNull(); + assertThat(vs.getVersion()).isNull(); + assertThat(vs.getVersionStartIncluding()).isEqualTo("2.13.0"); + assertThat(vs.getVersionStartExcluding()).isNull(); + assertThat(vs.getVersionEndIncluding()).isEqualTo("2.13.2.0"); + assertThat(vs.getVersionEndExcluding()).isNull(); + + final List attributions = qm.getAffectedVersionAttributions(vulnerability1, vs); + assertThat(attributions).satisfiesExactlyInAnyOrder( + attr -> assertThat(attr.getSource()).isEqualTo(Vulnerability.Source.GITHUB) + ); + }, + // The version range reported by both OSV and another source + // must have attributions for both sources. + vs -> { + assertThat(vs.getPurlType()).isEqualTo("composer"); + assertThat(vs.getPurlNamespace()).isEqualTo("tltneon"); + assertThat(vs.getPurlName()).isEqualTo("lgsl"); + assertThat(vs.getPurlVersion()).isNull(); + assertThat(vs.getVersion()).isNull(); + assertThat(vs.getVersionStartIncluding()).isNull(); + assertThat(vs.getVersionStartExcluding()).isNull(); + assertThat(vs.getVersionEndIncluding()).isNull(); + assertThat(vs.getVersionEndExcluding()).isEqualTo("7.0.0"); + + final List attributions = qm.getAffectedVersionAttributions(vulnerability1, vs); + assertThat(attributions).satisfiesExactlyInAnyOrder( + attr -> assertThat(attr.getSource()).isEqualTo(Vulnerability.Source.COMPOSER), + attr -> assertThat(attr.getSource()).isEqualTo(Vulnerability.Source.GITHUB) + ); + }, + // The version range newly reported by COMPOSER must be attributed to only COMPOSER. + vs -> { + assertThat(vs.getPurlType()).isEqualTo("composer"); + assertThat(vs.getPurlNamespace()).isEqualTo("tltneon"); + assertThat(vs.getPurlName()).isEqualTo("lgsl"); + assertThat(vs.getPurlVersion()).isNull(); + assertThat(vs.getVersion()).isNull(); + assertThat(vs.getVersionStartIncluding()).isEqualTo("4.3.0"); + assertThat(vs.getVersionStartExcluding()).isNull(); + assertThat(vs.getVersionEndIncluding()).isEqualTo("4.4.5"); + assertThat(vs.getVersionEndExcluding()).isNull(); + + final List attributions = qm.getAffectedVersionAttributions(vulnerability1, vs); + assertThat(attributions).satisfiesExactly( + attr -> assertThat(attr.getSource()).isEqualTo(Vulnerability.Source.COMPOSER) + ); + } + ); + + } + + @Test + public void testPackagistAdvisoriesNonExistingGHSA() throws Exception { + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + + Repository repo = setupPackagistAdvisoryMock(); + + Assert.assertTrue(task.mirrorAdvisories(qm, repo)); + Assert.assertEquals(10, qm.getVulnerabilities().getTotal()); + + Vulnerability vulnerability1 = qm.getVulnerabilityByVulnId(Vulnerability.Source.GITHUB, "GHSA-xx95-62h6-h7v3", true); + + Assert.assertNotNull(vulnerability1); + + Assert.assertEquals("lgsl Stored Cross-Site Scripting vulnerability", vulnerability1.getTitle()); + + final List vsList = vulnerability1.getVulnerableSoftware(); + assertThat(vsList).satisfiesExactlyInAnyOrder( + vs -> { + assertThat(vs.getPurlType()).isEqualTo("composer"); + assertThat(vs.getPurlNamespace()).isEqualTo("tltneon"); + assertThat(vs.getPurlName()).isEqualTo("lgsl"); + assertThat(vs.getPurlVersion()).isNull(); + assertThat(vs.getVersion()).isNull(); + assertThat(vs.getVersionStartIncluding()).isNull(); + assertThat(vs.getVersionStartExcluding()).isNull(); + assertThat(vs.getVersionEndIncluding()).isNull(); + assertThat(vs.getVersionEndExcluding()).isEqualTo("7.0.0"); + + final List attributions = qm.getAffectedVersionAttributions(vulnerability1, vs); + assertThat(attributions).satisfiesExactlyInAnyOrder( + attr -> assertThat(attr.getSource()).isEqualTo(Vulnerability.Source.COMPOSER) + ); + }, + vs -> { + assertThat(vs.getPurlType()).isEqualTo("composer"); + assertThat(vs.getPurlNamespace()).isEqualTo("tltneon"); + assertThat(vs.getPurlName()).isEqualTo("lgsl"); + assertThat(vs.getPurlVersion()).isNull(); + assertThat(vs.getVersion()).isNull(); + assertThat(vs.getVersionStartIncluding()).isEqualTo("4.3.0"); + assertThat(vs.getVersionStartExcluding()).isNull(); + assertThat(vs.getVersionEndIncluding()).isEqualTo("4.4.5"); + assertThat(vs.getVersionEndExcluding()).isNull(); + + final List attributions = qm.getAffectedVersionAttributions(vulnerability1, vs); + assertThat(attributions).satisfiesExactly( + attr -> assertThat(attr.getSource()).isEqualTo(Vulnerability.Source.COMPOSER) + ); + } + + ); + + } + + + private Repository setupDrupalAdvisoryMock() throws Exception { + final File packagistRepoRootFile = ComposerMetaAnalyzerTest.getRepoResourceFile("packages.drupal.org", "packages"); + final File advisoryFile = ComposerMetaAnalyzerTest.getRepoResourceFile("packages.drupal.org", "advisories"); + + @SuppressWarnings("resource") + MockServerClient mockClient = new MockServerClient("localhost", mockServer.getPort()); + String mockUrl = String.format("http://localhost:%d", mockServer.getPort()); + mockClient.when( + request() + .withMethod("GET") + .withPath("/packages.json")) + .respond( + response() + .withStatusCode(200) + .withHeader(HttpHeaders.CONTENT_TYPE, + "application/json") + .withBody(getRepoRootForMock(packagistRepoRootFile, mockUrl))); + + mockClient.when( + request() + .withMethod("GET") + .withPath("/api/security-advisories") + .withQueryStringParameter("updatedSince", "100") + ) + .respond( + response() + .withStatusCode(200) + .withHeader(HttpHeaders.CONTENT_TYPE, + "application/json") + .withBody(new String(ComposerMetaAnalyzerTest.getTestData(advisoryFile)))); + + return qm.createRepository(RepositoryType.COMPOSER, "drupal8", null, mockUrl, true, false, false, null, null, CONFIG_MIRROR_ENABLED_WITH_ALIAS); + } + + + @Test + public void testDrupalAdvisories() throws Exception { + ComposerAdvisoryMirrorTask task = new ComposerAdvisoryMirrorTask(); + + Repository repo = setupDrupalAdvisoryMock(); + + Assert.assertTrue(task.mirrorAdvisories(qm, repo)); + Assert.assertEquals(14, qm.getVulnerabilities().getTotal()); + + Vulnerability vulnerability1 = qm.getVulnerabilityByVulnId(Vulnerability.Source.DRUPAL, "SA-CORE-2018-002", true); + + Assert.assertNotNull(vulnerability1); + Assert.assertEquals("SA-CORE-2018-002", vulnerability1.getVulnId()); + Assert.assertEquals("SA-CORE-2018-002", vulnerability1.getAliases().get(0).getDrupalId()); + Assert.assertEquals("CVE-2018-7600", vulnerability1.getAliases().get(0).getCveId()); + Assert.assertNull(vulnerability1.getAliases().get(0).getComposerId()); + + Assert.assertEquals(">=7.0 <7.58", vulnerability1.getVulnerableVersions()); + Assert.assertEquals(1, vulnerability1.getVulnerableSoftware().size()); + Assert.assertEquals("7.0", vulnerability1.getVulnerableSoftware().get(0).getVersionStartIncluding()); + Assert.assertNull(vulnerability1.getVulnerableSoftware().get(0).getVersionStartExcluding()); + Assert.assertNull(vulnerability1.getVulnerableSoftware().get(0).getVersionEndIncluding()); + Assert.assertEquals("7.58", vulnerability1.getVulnerableSoftware().get(0).getVersionEndExcluding()); + } + + private String getRepoRootForMock(File file, String mockUrl) throws Exception { + String data = new String(ComposerMetaAnalyzerTest.getTestData(file)); + JSONObject json = new JSONObject(data); + + json.getJSONObject("security-advisories").put("api-url", mockUrl + "/api/security-advisories"); + return json.toString(); + } + + +} diff --git a/src/test/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzerTest.java b/src/test/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzerTest.java index b071dd5896..930b88b66f 100644 --- a/src/test/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzerTest.java +++ b/src/test/java/org/dependencytrack/tasks/repositories/ComposerMetaAnalyzerTest.java @@ -774,7 +774,7 @@ public void testAnalyzerGetsUnexpectedResponseContent404() throws Exception { Assert.assertNull(metaModel.getLatestVersion()); } - private static File getRepoResourceFile(String repo, String filename) throws Exception { + public static File getRepoResourceFile(String repo, String filename) throws Exception { String filenameResource = String.format( "unit/tasks/repositories/https---%s-%s.json", repo, @@ -782,7 +782,7 @@ private static File getRepoResourceFile(String repo, String filename) throws Exc return getFileResource(filenameResource); } - private static File getPackageResourceFile(String repo, String namespace, String name) throws Exception { + public static File getPackageResourceFile(String repo, String namespace, String name) throws Exception { String filename = String.format( "unit/tasks/repositories/https---%s-%s-%s.json", repo, @@ -791,18 +791,19 @@ private static File getPackageResourceFile(String repo, String namespace, String return getFileResource(filename); } - private static File getFileResource(String filename) throws Exception { + public static File getFileResource(String filename) throws Exception { return new File( Thread.currentThread().getContextClassLoader() .getResource(filename) .toURI()); } - private static byte[] getTestData(File file) throws Exception { + public static byte[] getTestData(File file) throws Exception { final FileInputStream fileStream = new FileInputStream(file); byte[] data = new byte[(int) file.length()]; fileStream.read(data); fileStream.close(); return data; } + } diff --git a/src/test/resources/unit/tasks/repositories/https---packages.drupal.org-advisories.json b/src/test/resources/unit/tasks/repositories/https---packages.drupal.org-advisories.json new file mode 100644 index 0000000000..d4cff79931 --- /dev/null +++ b/src/test/resources/unit/tasks/repositories/https---packages.drupal.org-advisories.json @@ -0,0 +1,244 @@ +{ + "advisories": { + "drupal/permissions_by_term": [ + { + "advisoryId": "SA-CONTRIB-2017-082", + "packageName": "drupal/permissions_by_term", + "title": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2017-082", + "link": "https://www.drupal.org/sa-contrib-2017-082", + "cve": null, + "affectedVersions": "\u003C1.35.0", + "reportedAt": "2017-11-08 17:16:30", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2017-082", + "remoteId": "SA-CONTRIB-2017-082" + } + ] + }, + { + "advisoryId": "SA-CONTRIB-2019-068", + "packageName": "drupal/permissions_by_term", + "title": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2019-068", + "link": "https://www.drupal.org/sa-contrib-2019-068", + "cve": null, + "affectedVersions": "\u003C2.11.0", + "reportedAt": "2019-09-25 14:43:49", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2019-068", + "remoteId": "SA-CONTRIB-2019-068" + } + ] + }, + { + "advisoryId": "SA-CONTRIB-2019-095", + "packageName": "drupal/permissions_by_term", + "title": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2019-095", + "link": "https://www.drupal.org/sa-contrib-2019-095", + "cve": null, + "affectedVersions": "\u003C2.0.0", + "reportedAt": "2019-12-11 18:59:46", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2019-095", + "remoteId": "SA-CONTRIB-2019-095" + } + ] + }, + { + "advisoryId": "SA-CONTRIB-2022-055", + "packageName": "drupal/permissions_by_term", + "title": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2022-055", + "link": "https://www.drupal.org/sa-contrib-2022-055", + "cve": null, + "affectedVersions": "\u003C3.1.19", + "reportedAt": "2022-09-07 17:04:31", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2022-055", + "remoteId": "SA-CONTRIB-2022-055" + } + ] + }, + { + "advisoryId": "SA-CONTRIB-2022-056", + "packageName": "drupal/permissions_by_term", + "title": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2022-056", + "link": "https://www.drupal.org/sa-contrib-2022-056", + "cve": null, + "affectedVersions": "\u003C3.1.19", + "reportedAt": "2022-09-07 17:06:06", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Permissions by Term - Moderately critical - Access bypass - SA-CONTRIB-2022-056", + "remoteId": "SA-CONTRIB-2022-056" + } + ] + } + ], + "drupal/config_perms": [ + { + "advisoryId": "SA-CONTRIB-2017-083", + "packageName": "drupal/config_perms", + "title": "Custom Permissions - Moderately critical - Access bypass - SA-CONTRIB-2017-083", + "link": "https://www.drupal.org/sa-contrib-2017-083", + "cve": null, + "affectedVersions": "\u003C1.1.0", + "reportedAt": "2017-11-08 17:22:08", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Custom Permissions - Moderately critical - Access bypass - SA-CONTRIB-2017-083", + "remoteId": "SA-CONTRIB-2017-083" + } + ] + }, + { + "advisoryId": "SA-CONTRIB-2019-055", + "packageName": "drupal/config_perms", + "title": "Custom Permissions - Critical - Access bypass - SA-CONTRIB-2019-055", + "link": "https://www.drupal.org/sa-contrib-2019-055", + "cve": null, + "affectedVersions": "\u003C1.2.0", + "reportedAt": "2019-07-10 16:30:00", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Custom Permissions - Critical - Access bypass - SA-CONTRIB-2019-055", + "remoteId": "SA-CONTRIB-2019-055" + } + ] + } + ], + "drupal/config_update": [ + { + "advisoryId": "SA-CONTRIB-2017-091", + "packageName": "drupal/config_update", + "title": "Configuration Update Manager - Moderately critical - Cross Site Request Forgery (CSRF) - SA-CONTRIB-2017-091", + "link": "https://www.drupal.org/sa-contrib-2017-091", + "cve": null, + "affectedVersions": "\u003C1.5", + "reportedAt": "2017-12-06 18:44:03", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Configuration Update Manager - Moderately critical - Cross Site Request Forgery (CSRF) - SA-CONTRIB-2017-091", + "remoteId": "SA-CONTRIB-2017-091" + } + ] + } + ], + "drupal/link_click_count": [ + { + "advisoryId": "SA-CONTRIB-2017-094", + "packageName": "drupal/link_click_count", + "title": "Link Click Count - Critical - Unsupported - SA-CONTRIB-2017-094", + "link": "https://www.drupal.org/sa-contrib-2017-094", + "cve": null, + "affectedVersions": "*", + "reportedAt": "2017-12-20 14:12:47", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Link Click Count - Critical - Unsupported - SA-CONTRIB-2017-094", + "remoteId": "SA-CONTRIB-2017-094" + } + ] + } + ], + "drupal/stacks": [ + { + "advisoryId": "SA-CONTRIB-2018-001", + "packageName": "drupal/stacks", + "title": "Stacks - Critical - Arbitrary PHP code execution - SA-CONTRIB-2018-001", + "link": "https://www.drupal.org/sa-contrib-2018-001", + "cve": null, + "affectedVersions": "\u003C1.1.0", + "reportedAt": "2018-01-10 17:57:53", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Stacks - Critical - Arbitrary PHP code execution - SA-CONTRIB-2018-001", + "remoteId": "SA-CONTRIB-2018-001" + } + ] + } + ], + "drupal/node_view_permissions": [ + { + "advisoryId": "SA-CONTRIB-2018-002", + "packageName": "drupal/node_view_permissions", + "title": "Node View Permissions - Moderately critical - Access Bypass - SA-CONTRIB-2018-002", + "link": "https://www.drupal.org/sa-contrib-2018-002", + "cve": null, + "affectedVersions": "\u003C1.1.0", + "reportedAt": "2018-01-10 18:02:19", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Node View Permissions - Moderately critical - Access Bypass - SA-CONTRIB-2018-002", + "remoteId": "SA-CONTRIB-2018-002" + } + ] + } + ], + "drupal/entity_ref_tab_formatter": [ + { + "advisoryId": "SA-CONTRIB-2018-008", + "packageName": "drupal/entity_ref_tab_formatter", + "title": "Entity Reference Tab / Accordion Formatter - Moderately critical - Cross Site Scripting - SA-CONTRIB-2018-008", + "link": "https://www.drupal.org/sa-contrib-2018-008", + "cve": null, + "affectedVersions": "\u003C1.3.0", + "reportedAt": "2018-02-07 18:45:12", + "composerRepository": "http://packages.drupal.org/8/", + "sources": [ + { + "name": "Entity Reference Tab / Accordion Formatter - Moderately critical - Cross Site Scripting - SA-CONTRIB-2018-008", + "remoteId": "SA-CONTRIB-2018-008" + } + ] + } + ], + "drupal/core": [ + { + "advisoryId": "SA-CORE-2018-001", + "packageName": "drupal/core", + "title": "Drupal core - Critical - Multiple Vulnerabilities - SA-CORE-2018-001", + "link": "https://www.drupal.org/sa-core-2018-001", + "cve": null, + "affectedVersions": "\u003E=7.0 \u003C7.57 || \u003E= 8.0.0 \u003C8.4.5", + "reportedAt": "2018-02-21 17:10:55", + "composerRepository": "https://packagist.org/", + "sources": [ + { + "name": "Drupal core - Critical - Multiple Vulnerabilities - SA-CORE-2018-001", + "remoteId": "SA-CORE-2018-001" + } + ] + }, + { + "advisoryId": "SA-CORE-2018-002", + "packageName": "drupal/core", + "title": "Drupal core - Highly critical - Remote Code Execution - SA-CORE-2018-002", + "link": "https://www.drupal.org/sa-core-2018-002", + "cve": "CVE-2018-7600", + "affectedVersions": "\u003E=7.0 \u003C7.58", + "reportedAt": "2018-03-28 18:14:10", + "composerRepository": "https://packagist.org/", + "sources": [ + { + "name": "Drupal core - Highly critical - Remote Code Execution - SA-CORE-2018-002", + "remoteId": "SA-CORE-2018-002" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/unit/tasks/repositories/https---repo.packagist.org-advisories.json b/src/test/resources/unit/tasks/repositories/https---repo.packagist.org-advisories.json new file mode 100644 index 0000000000..c15eb91ea7 --- /dev/null +++ b/src/test/resources/unit/tasks/repositories/https---repo.packagist.org-advisories.json @@ -0,0 +1,200 @@ +{ + "advisories": { + "nilsteampassnet/teampass": [ + { + "advisoryId": "PKSA-q4rt-5vfc-wksb", + "packageName": "nilsteampassnet/teampass", + "remoteId": "GHSA-2697-96mv-3gfm", + "title": "TeamPass does not properly check whether a folder is in a user's allowed folders list", + "link": "https://github.com/advisories/GHSA-2697-96mv-3gfm", + "cve": "CVE-2024-50701", + "affectedVersions": "\u003C3.1.3.1", + "source": "GitHub", + "reportedAt": "2024-12-30 15:31:59", + "composerRepository": "https://packagist.org", + "severity": "medium", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-2697-96mv-3gfm" + } + ] + }, + { + "advisoryId": "PKSA-r8k5-qv9m-hf6j", + "packageName": "nilsteampassnet/teampass", + "remoteId": "GHSA-7rm3-4w6j-8xx4", + "title": "TeamPass mail_me operation authorization issue", + "link": "https://github.com/advisories/GHSA-7rm3-4w6j-8xx4", + "cve": "CVE-2024-50702", + "affectedVersions": "\u003C3.1.3.1", + "source": "GitHub", + "reportedAt": "2024-12-30 15:31:59", + "composerRepository": "https://packagist.org", + "severity": "medium", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-7rm3-4w6j-8xx4" + } + ] + }, + { + "advisoryId": "PKSA-x31v-w4h8-4xrb", + "packageName": "nilsteampassnet/teampass", + "remoteId": "GHSA-9wmc-988h-2mv2", + "title": "TeamPass privileges issue", + "link": "https://github.com/advisories/GHSA-9wmc-988h-2mv2", + "cve": "CVE-2024-50703", + "affectedVersions": "\u003C3.1.3.1", + "source": "GitHub", + "reportedAt": "2024-12-30 15:31:59", + "composerRepository": "https://packagist.org", + "severity": "critical", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-9wmc-988h-2mv2" + } + ] + }, + { + "advisoryId": "PKSA-tmqs-sp77-dd7y", + "packageName": "nilsteampassnet/teampass", + "remoteId": "GHSA-28pv-2j2h-fmhc", + "title": "TeamPass Cross-Site Scripting (XSS)", + "link": "https://github.com/advisories/GHSA-28pv-2j2h-fmhc", + "cve": "CVE-2017-15278", + "affectedVersions": "\u003C2.1.27.9", + "source": "GitHub", + "reportedAt": "2022-05-17 00:29:54", + "composerRepository": "https://packagist.org", + "severity": "medium", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-28pv-2j2h-fmhc" + } + ] + } + ], + "dcat/laravel-admin": [ + { + "advisoryId": "PKSA-6p1r-xpgk-hcz2", + "packageName": "dcat/laravel-admin", + "remoteId": "GHSA-9q34-7hfr-h8jm", + "title": "Dcat Admin Cross-site Scripting (XSS) vulnerability", + "link": "https://github.com/advisories/GHSA-9q34-7hfr-h8jm", + "cve": "CVE-2024-54774", + "affectedVersions": "=2.2.0-beta", + "source": "GitHub", + "reportedAt": "2024-12-28 00:30:43", + "composerRepository": "https://packagist.org", + "severity": "medium", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-9q34-7hfr-h8jm" + } + ] + }, + { + "advisoryId": "PKSA-m82y-5rmz-8w8p", + "packageName": "dcat/laravel-admin", + "remoteId": "GHSA-37x3-j9jq-vrjx", + "title": "Dcat-Admin Cross-Site Scripting (XSS) vulnerability", + "link": "https://github.com/advisories/GHSA-37x3-j9jq-vrjx", + "cve": "CVE-2024-54775", + "affectedVersions": "=2.2.2-beta|=2.2.0-beta", + "source": "GitHub", + "reportedAt": "2024-12-28 00:30:43", + "composerRepository": "https://packagist.org", + "severity": "medium", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-37x3-j9jq-vrjx" + } + ] + }, + { + "advisoryId": "PKSA-98qy-9244-htqs", + "packageName": "dcat/laravel-admin", + "remoteId": "GHSA-mr24-cf69-5chq", + "title": "dcat-admin Cross Site Scripting vulnerability", + "link": "https://github.com/advisories/GHSA-mr24-cf69-5chq", + "cve": "CVE-2024-29644", + "affectedVersions": "\u003C=2.1.3", + "source": "GitHub", + "reportedAt": "2024-03-26 12:31:28", + "composerRepository": "https://packagist.org", + "severity": "medium", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-mr24-cf69-5chq" + } + ] + }, + { + "advisoryId": "PKSA-1b2w-knm8-9nnm", + "packageName": "dcat/laravel-admin", + "remoteId": "GHSA-p74v-mwvg-8ghp", + "title": "Dcat-Admin vulnerable to Stored Cross-site Scripting", + "link": "https://github.com/advisories/GHSA-p74v-mwvg-8ghp", + "cve": "CVE-2023-33736", + "affectedVersions": "\u003C=2.1.3-beta", + "source": "GitHub", + "reportedAt": "2023-05-31 15:30:18", + "composerRepository": "https://packagist.org", + "severity": "medium", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-p74v-mwvg-8ghp" + } + ] + } + ], + "tltneon/lgsl": [ + { + "advisoryId": "PKSA-81bx-8rdn-n435", + "packageName": "tltneon/lgsl", + "remoteId": "GHSA-ggwq-xc72-33r3", + "title": "LGSL has a reflected XSS at /lgsl_files/lgsl_list.php", + "link": "https://github.com/advisories/GHSA-ggwq-xc72-33r3", + "cve": "CVE-2024-56517", + "affectedVersions": "\u003C=6.2.1", + "source": "GitHub", + "reportedAt": "2024-12-30 16:49:28", + "composerRepository": "https://packagist.org", + "severity": "medium", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-ggwq-xc72-33r3" + } + ] + }, + { + "advisoryId": "PKSA-jsdk-m8fr-4rfv", + "packageName": "tltneon/lgsl", + "remoteId": "GHSA-xx95-62h6-h7v3", + "title": "lgsl Stored Cross-Site Scripting vulnerability", + "link": "https://github.com/advisories/GHSA-xx95-62h6-h7v3", + "cve": "CVE-2024-56361", + "affectedVersions": "\u003E=4.3.0,\u003C=4.4.5|\u003C7.0.0", + "source": "GitHub", + "reportedAt": "2024-12-26 20:20:12", + "composerRepository": "https://packagist.org", + "severity": "high", + "sources": [ + { + "name": "GitHub", + "remoteId": "GHSA-xx95-62h6-h7v3" + } + ] + } + ] + } + } \ No newline at end of file