diff --git a/docs/_docs/analysis-types/known-vulnerabilities.md b/docs/_docs/analysis-types/known-vulnerabilities.md index c7f5cb68de..e3919d5149 100644 --- a/docs/_docs/analysis-types/known-vulnerabilities.md +++ b/docs/_docs/analysis-types/known-vulnerabilities.md @@ -22,6 +22,9 @@ The internal analyzer relies on a dictionary of vulnerable software. This dictio NVD, GitHub Advisories, or VulnDB mirroring is performed. The internal analyzer is applicable to all components with valid CPEs, including application, operating system, and hardware components, and all components with Package URLs. +**NOTE**: Currently, vulnerable software describing affected package is treated as 'append-only' meaning there might be some entries no longer reported by the vulnerability source. +Fix is under progress and can be tracked via issue [#1815](https://github.com/DependencyTrack/dependency-track/issues/1815). + ### OSS Index Analyzer OSS Index is a service provided by Sonatype which identifies vulnerabilities in third-party components. The service diff --git a/docs/_docs/datasources/internal-components.md b/docs/_docs/datasources/internal-components.md index 13f9fa1fd0..f699fedf8f 100644 --- a/docs/_docs/datasources/internal-components.md +++ b/docs/_docs/datasources/internal-components.md @@ -2,7 +2,7 @@ title: Internal Components category: Datasources chapter: 4 -order: 8 +order: 9 --- Organizations have the ability to specify a namespace and/or name which represents internally diff --git a/docs/_docs/datasources/private-vuln-repo.md b/docs/_docs/datasources/private-vuln-repo.md index 575448e0ba..ff79cc7a73 100644 --- a/docs/_docs/datasources/private-vuln-repo.md +++ b/docs/_docs/datasources/private-vuln-repo.md @@ -2,7 +2,7 @@ title: Private Vulnerability Repository category: Datasources chapter: 4 -order: 9 +order: 10 redirect_from: - /usage/private-vuln-repo/ --- diff --git a/docs/_docs/datasources/repositories.md b/docs/_docs/datasources/repositories.md index f87dae8086..b8644bc5b4 100644 --- a/docs/_docs/datasources/repositories.md +++ b/docs/_docs/datasources/repositories.md @@ -2,7 +2,7 @@ title: Repositories category: Datasources chapter: 4 -order: 7 +order: 8 --- Dependency-Track relies on integration with repositories to help identify metadata that may be useful diff --git a/docs/_docs/datasources/routing.md b/docs/_docs/datasources/routing.md index 491073fc50..9f44414b3a 100644 --- a/docs/_docs/datasources/routing.md +++ b/docs/_docs/datasources/routing.md @@ -2,7 +2,7 @@ title: Datasource Routing category: Datasources chapter: 4 -order: 6 +order: 7 --- Components often belong to one or more ecosystems. These ecosystems typically have one or more sources of diff --git a/docs/_docs/datasources/snyk.md b/docs/_docs/datasources/snyk.md new file mode 100644 index 0000000000..d0926bef9e --- /dev/null +++ b/docs/_docs/datasources/snyk.md @@ -0,0 +1,41 @@ +--- +title: Snyk +category: Datasources +chapter: 4 +order: 5 +--- + +[Snyk](https://security.snyk.io) is a platform allowing you to scan, prioritize, and fix security vulnerabilities in your own code, open source dependencies, container images, and Infrastructure as Code (IaC) configurations. + +It is a developer security platform. Integrating directly into development tools, workflows, and automation pipelines, Snyk makes it easy for teams to find, prioritize, and fix security vulnerabilities in code, dependencies, containers, and infrastructure as code. Supported by industry-leading application and security intelligence, Snyk puts security expertise in any developer's toolkit. + +Dependency-Track integrates with Snyk using its [REST API](https://apidocs.snyk.io/). Dependency-Track does not mirror Snyk entirely, +but it does consume vulnerabilities on a 'as-identified' basis. + +The Snyk integration is disabled by default. + +### Authentication + +User must get API token from Snyk. You can find your token in your [General Account Settings](https://snyk.io/account/) after you register with Snyk and log in. See [Authentication for API](https://docs.snyk.io/snyk-api-info/authentication-for-api). + +Provide the token (**without** 'token' prefixed) in the configuration as shown below. + +### Configuration + +**Organization ID** can be set at in the [Settings](https://docs.snyk.io/products/snyk-code/cli-for-snyk-code/before-you-start-set-the-organization-for-the-cli-tests/finding-the-snyk-id-and-internal-name-of-an-organization) page of the Organization on the Web UI. + +**Snyk base URL** is set by default, can be changed per requirement. + +**Snyk API version** is set by default to latest version. It is updated every 6 months and might get expired causing API communication failure in which case it will be updated in next upcoming DT release. +User can change it manually here. Please refer [API](https://apidocs.snyk.io/?version=2022-10-06#overview) to submit the correct version. +**Number of threads for Snyk Analyzer to use** Snyk analyzer is implemented with multithreading model to complete the analysis faster. The number of threads that would be used is configurable. By default, it is set to 10. The value can be overridden by exporting this environment variable: `SNYK_THREAD_BATCH_SIZE`. The value can be set based on the configuration of the machine. + +![](../../images/snyk-configuration.png) + +### Understanding Snyk's CVSS analysis + +The majority of vulnerabilities published by Snyk originate from proprietary research, public information sources, or through 3rd party disclosures. + +When evaluating the severity of a vulnerability, it's important to note that there is no single CVSS vector - there are multiple CVSS vectors defined by multiple vendors, with the National Vulnerability Database (NVD) being one of them. + +**NOTE:** For Beta version, user can select either from NVD or SNYK to prioritize the cvss vectors. \ No newline at end of file diff --git a/docs/_docs/datasources/vulndb.md b/docs/_docs/datasources/vulndb.md index 96fea099b1..025d6f4196 100644 --- a/docs/_docs/datasources/vulndb.md +++ b/docs/_docs/datasources/vulndb.md @@ -2,7 +2,7 @@ title: VulnDB category: Datasources chapter: 4 -order: 5 +order: 6 --- VulnDB, a subscription service offered by Risk Based Security, offers a comprehensive and continuously updated diff --git a/docs/images/snyk-configuration.png b/docs/images/snyk-configuration.png new file mode 100644 index 0000000000..2a89b97d5d Binary files /dev/null and b/docs/images/snyk-configuration.png differ diff --git a/src/main/java/org/dependencytrack/common/ConfigKey.java b/src/main/java/org/dependencytrack/common/ConfigKey.java new file mode 100644 index 0000000000..d7b40439f6 --- /dev/null +++ b/src/main/java/org/dependencytrack/common/ConfigKey.java @@ -0,0 +1,26 @@ +package org.dependencytrack.common; + +import alpine.Config; + +public enum ConfigKey implements Config.Key{ + SNYK_THREAD_BATCH_SIZE("snyk.thread.batch.size", 10); + + private final String propertyName; + private final Object defaultValue; + + ConfigKey(final String propertyName, final Object defaultValue) { + this.propertyName = propertyName; + this.defaultValue = defaultValue; + } + + @Override + public String getPropertyName() { + return propertyName; + } + + @Override + public Object getDefaultValue() { + return defaultValue; + } + +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index 1f57ca04a9..2d70b69a5f 100644 --- a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -49,6 +49,7 @@ import org.dependencytrack.tasks.repositories.RepositoryMetaAnalyzerTask; import org.dependencytrack.tasks.scanners.InternalAnalysisTask; import org.dependencytrack.tasks.scanners.OssIndexAnalysisTask; +import org.dependencytrack.tasks.scanners.SnykAnalysisTask; import org.dependencytrack.tasks.scanners.VulnDbAnalysisTask; import javax.servlet.ServletContextEvent; @@ -91,6 +92,7 @@ public void contextInitialized(final ServletContextEvent event) { EVENT_SERVICE.subscribe(VulnDbAnalysisEvent.class, VulnDbAnalysisTask.class); EVENT_SERVICE.subscribe(VulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class); EVENT_SERVICE.subscribe(PortfolioVulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class); + EVENT_SERVICE.subscribe(SnykAnalysisEvent.class, SnykAnalysisTask.class); EVENT_SERVICE.subscribe(RepositoryMetaEvent.class, RepositoryMetaAnalyzerTask.class); EVENT_SERVICE.subscribe(ComponentMetricsUpdateEvent.class, ComponentMetricsUpdateTask.class); EVENT_SERVICE.subscribe(ProjectMetricsUpdateEvent.class, ProjectMetricsUpdateTask.class); @@ -135,6 +137,7 @@ public void contextDestroyed(final ServletContextEvent event) { EVENT_SERVICE.unsubscribe(ProjectMetricsUpdateTask.class); EVENT_SERVICE.unsubscribe(PortfolioMetricsUpdateTask.class); EVENT_SERVICE.unsubscribe(VulnerabilityMetricsUpdateTask.class); + EVENT_SERVICE.unsubscribe(SnykAnalysisTask.class); EVENT_SERVICE.unsubscribe(CloneProjectTask.class); EVENT_SERVICE.unsubscribe(FortifySscUploadTask.class); EVENT_SERVICE.unsubscribe(DefectDojoUploadTask.class); diff --git a/src/main/java/org/dependencytrack/event/SnykAnalysisEvent.java b/src/main/java/org/dependencytrack/event/SnykAnalysisEvent.java new file mode 100644 index 0000000000..4245158501 --- /dev/null +++ b/src/main/java/org/dependencytrack/event/SnykAnalysisEvent.java @@ -0,0 +1,22 @@ +package org.dependencytrack.event; + +import org.dependencytrack.model.Component; + +import java.util.List; + +/** + * Defines an event used to start an analysis via Snyk REST API. + */ +public class SnykAnalysisEvent extends VulnerabilityAnalysisEvent { + + public SnykAnalysisEvent() { } + + public SnykAnalysisEvent(final Component component) { + super(component); + } + + public SnykAnalysisEvent(final List components) { + super(components); + } + +} diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index abdb4aa937..3c89ba4010 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -46,7 +46,13 @@ public enum ConfigPropertyConstants { SCANNER_VULNDB_ENABLED("scanner", "vulndb.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable VulnDB"), SCANNER_VULNDB_OAUTH1_CONSUMER_KEY("scanner", "vulndb.api.oauth1.consumerKey", null, PropertyType.STRING, "The OAuth 1.0a consumer key"), SCANNER_VULNDB_OAUTH1_CONSUMER_SECRET("scanner", "vulndb.api.oath1.consumerSecret", null, PropertyType.ENCRYPTEDSTRING, "The OAuth 1.0a consumer secret"), - SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD("scanner", "analysis.cache.validity.period","864000", PropertyType.NUMBER, "Validity period for individual component analysis cache"), + SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD("scanner", "analysis.cache.validity.period", "864000", PropertyType.NUMBER, "Validity period for individual component analysis cache"), + SCANNER_SNYK_ENABLED("scanner", "snyk.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable Snyk Vulnerability Analysis"), + SCANNER_SNYK_API_TOKEN("scanner", "snyk.api.token", null, PropertyType.ENCRYPTEDSTRING, "The API token used for Snyk API authentication"), + SCANNER_SNYK_ORG_ID("scanner", "snyk.org.id", null, PropertyType.STRING, "The Organization ID used for Snyk API access"), + SCANNER_SNYK_API_VERSION("scanner", "snyk.api.version", "2022-09-15", PropertyType.STRING, "Snyk API version"), + SCANNER_SNYK_CVSS_SOURCE("scanner", "snyk.cvss.source", "NVD", PropertyType.STRING, "Type of source to be prioritized for cvss calculation"), + SCANNER_SNYK_BASE_URL("scanner", "snyk.base.url", "https://api.snyk.io", PropertyType.URL, "Base Url pointing to the hostname and path for Snyk analysis"), VULNERABILITY_SOURCE_NVD_ENABLED("vuln-source", "nvd.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable National Vulnerability Database"), VULNERABILITY_SOURCE_NVD_FEEDS_URL("vuln-source", "nvd.feeds.url", "https://nvd.nist.gov/feeds", PropertyType.URL, "A base URL pointing to the hostname and path of the NVD feeds"), VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ENABLED("vuln-source", "github.advisories.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitHub Advisories"), @@ -72,17 +78,17 @@ public enum ConfigPropertyConstants { ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio"), 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_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_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"), - TASK_SCHEDULER_REPOSITORY_METADATA_FETCH_CADENCE("task-scheduler", "repository.metadata.fetch.cadence","24", PropertyType.INTEGER, "Metadada fetch cadence (in hours) for package repositories"), - TASK_SCHEDULER_INTERNAL_COMPONENT_IDENTIFICATION_CADENCE("task-scheduler", "internal.components.identification.cadence","6", PropertyType.INTEGER, "Internal component identification cadence (in hours)"), - TASK_SCHEDULER_COMPONENT_ANALYSIS_CACHE_CLEAR_CADENCE("task-scheduler", "component.analysis.cache.clear.cadence","72", PropertyType.INTEGER, "Cleanup cadence (in hours) for component analysis cache"); + 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_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_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"), + TASK_SCHEDULER_REPOSITORY_METADATA_FETCH_CADENCE("task-scheduler", "repository.metadata.fetch.cadence", "24", PropertyType.INTEGER, "Metadada fetch cadence (in hours) for package repositories"), + TASK_SCHEDULER_INTERNAL_COMPONENT_IDENTIFICATION_CADENCE("task-scheduler", "internal.components.identification.cadence", "6", PropertyType.INTEGER, "Internal component identification cadence (in hours)"), + TASK_SCHEDULER_COMPONENT_ANALYSIS_CACHE_CLEAR_CADENCE("task-scheduler", "component.analysis.cache.clear.cadence", "72", PropertyType.INTEGER, "Cleanup cadence (in hours) for component analysis cache"); private String groupName; private String propertyName; diff --git a/src/main/java/org/dependencytrack/model/SnykCvssSource.java b/src/main/java/org/dependencytrack/model/SnykCvssSource.java new file mode 100644 index 0000000000..6d52d98d7c --- /dev/null +++ b/src/main/java/org/dependencytrack/model/SnykCvssSource.java @@ -0,0 +1,9 @@ +package org.dependencytrack.model; + +public enum SnykCvssSource { + + NVD, + SNYK, + RHEL, + SUSE +} diff --git a/src/main/java/org/dependencytrack/model/Vulnerability.java b/src/main/java/org/dependencytrack/model/Vulnerability.java index 85e46d289e..28d5a38645 100644 --- a/src/main/java/org/dependencytrack/model/Vulnerability.java +++ b/src/main/java/org/dependencytrack/model/Vulnerability.java @@ -110,7 +110,8 @@ public enum Source { OSSINDEX, // Sonatype OSS Index RETIREJS, // Retire.js INTERNAL, // Internally-managed (and manually entered) vulnerability - OSV // Google OSV Advisories + OSV, // Google OSV Advisories + SNYK, // Snyk Purl Vulnerability } @PrimaryKey diff --git a/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java b/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java index 7b52cf29ec..3a1d425c40 100644 --- a/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java +++ b/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java @@ -90,6 +90,13 @@ public class VulnerabilityAlias implements Serializable { @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The osvId field may only contain printable characters") private String osvId; + @Persistent + @Column(name = "SNYK_ID") + @Index(name = "VULNERABILITYALIAS_SNYK_ID_IDX") + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The snykId field may only contain printable characters") + private String snykId; + @Persistent @Column(name = "GSD_ID") @Index(name = "VULNERABILITYALIAS_GSD_ID_IDX") @@ -158,6 +165,10 @@ public void setOsvId(String osvId) { this.osvId = osvId; } + public String getSnykId() { return snykId; } + + public void setSnykId(String snykId) { this.snykId = snykId; } + public String getGsdId() { return gsdId; } diff --git a/src/main/java/org/dependencytrack/parser/snyk/SnykParser.java b/src/main/java/org/dependencytrack/parser/snyk/SnykParser.java new file mode 100644 index 0000000000..8edc563194 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/snyk/SnykParser.java @@ -0,0 +1,252 @@ +package org.dependencytrack.parser.snyk; + +import alpine.common.logging.Logger; +import alpine.model.ConfigProperty; +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerableSoftware; +import org.dependencytrack.model.Cwe; +import org.dependencytrack.model.SnykCvssSource; +import org.dependencytrack.model.VulnerabilityAlias; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.parser.common.resolver.CweResolver; +import org.dependencytrack.persistence.QueryManager; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Date; +import java.util.Collections; +import java.util.Arrays; +import static org.dependencytrack.util.JsonUtil.jsonStringToTimestamp; + +public class SnykParser { + private static final Logger LOGGER = Logger.getLogger(SnykParser.class); + + public Vulnerability parse(JSONArray data, QueryManager qm, String purl, int count) { + Vulnerability synchronizedVulnerability = new Vulnerability(); + Vulnerability vulnerability = new Vulnerability(); + List vsList = new ArrayList<>(); + vulnerability.setSource(Vulnerability.Source.SNYK); + // get the id of the data record (vulnerability) + vulnerability.setVulnId(data.optJSONObject(count).optString("id", null)); + final JSONObject vulnAttributes = data.optJSONObject(count).optJSONObject("attributes"); + if (vulnAttributes != null && vulnAttributes.optString("type").equalsIgnoreCase("package_vulnerability")) { + // get the references of the data record (vulnerability) + final JSONObject slots = vulnAttributes.optJSONObject("slots"); + if (slots != null && slots.optJSONArray("references") != null) { + vulnerability.setReferences(addReferences(slots)); + } + vulnerability.setTitle(vulnAttributes.optString("title", null)); + vulnerability.setDescription(vulnAttributes.optString("description", null)); + vulnerability.setCreated(Date.from(jsonStringToTimestamp(vulnAttributes.optString("created_at")).toInstant())); + vulnerability.setUpdated(Date.from(jsonStringToTimestamp(vulnAttributes.optString("updated_at")).toInstant())); + final JSONArray problems = vulnAttributes.optJSONArray("problems"); + if (problems != null) { + vulnerability.setAliases(computeAliases(vulnerability, qm, problems)); + } + final JSONArray cvssArray = vulnAttributes.optJSONArray("severities"); + vulnerability = cvssArray != null ? setCvssScore(cvssArray, vulnerability) : vulnerability; + JSONArray coordinates = vulnAttributes.optJSONArray("coordinates"); + if (coordinates != null) { + + for (int countCoordinates = 0; countCoordinates < coordinates.length(); countCoordinates++) { + JSONArray representation = coordinates.getJSONObject(countCoordinates).optJSONArray("representation"); + if (representation != null) { + vsList = parseVersionRanges(qm, purl, representation); + } + } + } + qm.persist(vsList); + synchronizedVulnerability = qm.synchronizeVulnerability(vulnerability, false); + synchronizedVulnerability.setVulnerableSoftware(vsList); + qm.persist(synchronizedVulnerability); + } + return synchronizedVulnerability; + } + + public List computeAliases(Vulnerability vulnerability, QueryManager qm, JSONArray problems) { + List vulnerabilityAliasList = new ArrayList<>(); + for (int i = 0; i < problems.length(); i++) { + final JSONObject problem = problems.optJSONObject(i); + String source = problem.optString("source"); + String id = problem.optString("id"); + // CWE + if (source.equalsIgnoreCase("CWE")) { + final Cwe cwe = CweResolver.getInstance().resolve(qm, id); + if (cwe != null) { + vulnerability.addCwe(cwe); + } + } + // CVE alias + else if (source.equalsIgnoreCase("CVE")) { + final VulnerabilityAlias vulnerabilityAlias = new VulnerabilityAlias(); + vulnerabilityAlias.setSnykId(vulnerability.getVulnId()); + vulnerabilityAlias.setCveId(id); + qm.synchronizeVulnerabilityAlias(vulnerabilityAlias); + vulnerabilityAliasList.add(vulnerabilityAlias); + } + // Github alias + else if (source.equalsIgnoreCase("GHSA")) { + final VulnerabilityAlias vulnerabilityAlias = new VulnerabilityAlias(); + vulnerabilityAlias.setSnykId(vulnerability.getVulnId()); + vulnerabilityAlias.setGhsaId(id); + qm.synchronizeVulnerabilityAlias(vulnerabilityAlias); + vulnerabilityAliasList.add(vulnerabilityAlias); + } + } + return vulnerabilityAliasList; + } + + public Vulnerability setCvssScore(JSONArray cvssArray, Vulnerability vulnerability) { + JSONObject cvss = selectCvssObjectBasedOnSource(cvssArray); + if (cvss != null) { + String severity = cvss.optString("level", null); + if (severity != null) { + if (severity.equalsIgnoreCase("CRITICAL")) { + vulnerability.setSeverity(Severity.CRITICAL); + } else if (severity.equalsIgnoreCase("HIGH")) { + vulnerability.setSeverity(Severity.HIGH); + } else if (severity.equalsIgnoreCase("MEDIUM")) { + vulnerability.setSeverity(Severity.MEDIUM); + } else if (severity.equalsIgnoreCase("LOW")) { + vulnerability.setSeverity(Severity.LOW); + } else { + vulnerability.setSeverity(Severity.UNASSIGNED); + } + } + vulnerability.setCvssV3Vector(cvss.optString("vector", null)); + final String cvssScore = cvss.optString("score"); + if (cvssScore != null) { + vulnerability.setCvssV3BaseScore(BigDecimal.valueOf(Double.parseDouble(cvssScore))); + } + } + return vulnerability; + } + + public String addReferences(JSONObject slots) { + final JSONArray links = slots.optJSONArray("references"); + final StringBuilder sb = new StringBuilder(); + for (int linkCount = 0; linkCount < links.length(); linkCount++) { + final JSONObject link = links.getJSONObject(linkCount); + String reference = link.optString("url", null); + if (reference != null) { + sb.append("* [").append(reference).append("](").append(reference).append(")\n"); + } + } + return sb.toString(); + } + + public JSONObject selectCvssObjectBasedOnSource(JSONArray cvssArray) { + + String cvssSourceHigh = getSnykCvssConfig(ConfigPropertyConstants.SCANNER_SNYK_CVSS_SOURCE); + String cvssSourceLow = cvssSourceHigh.equalsIgnoreCase(SnykCvssSource.NVD.toString()) ? + SnykCvssSource.SNYK.toString() : + SnykCvssSource.NVD.toString(); + JSONObject cvss = cvssArray.optJSONObject(0); + if (cvssArray.length() > 1) { + for (int i = 0; i < cvssArray.length(); i++) { + final JSONObject cvssObject = cvssArray.optJSONObject(i); + String source = cvssObject.optString("source"); + String vector = cvssObject.optString("vector"); + String score = cvssObject.optString("score"); + if (!source.isBlank() && !vector.isBlank() && !score.isBlank()) { + if (source.equalsIgnoreCase(cvssSourceHigh)) { + return cvssObject; + } + if (source.equalsIgnoreCase(cvssSourceLow)) { + cvss = cvssObject; + } else { + if (cvss != null && !cvss.optString("source").equalsIgnoreCase(cvssSourceLow)) { + cvss = cvssObject; + } + } + } + } + } + return cvss; + } + + public List parseVersionRanges(final QueryManager qm, final String purl, final JSONArray ranges) { + + List vulnerableSoftwares = new ArrayList<>(); + if (purl == null) { + LOGGER.debug("No PURL provided - skipping"); + return Collections.emptyList(); + } + + final PackageURL packageURL; + try { + packageURL = new PackageURL(purl); + } catch (MalformedPackageURLException ex) { + LOGGER.debug("Invalid PURL " + purl + " - skipping", ex); + return Collections.emptyList(); + } + + for (int i = 0; i < ranges.length(); i++) { + + String range = ranges.optString(i); + String versionStartIncluding = null; + String versionStartExcluding = null; + String versionEndIncluding = null; + String versionEndExcluding = null; + final String[] parts; + + if (range.contains(",")) { + parts = Arrays.stream(range.split(",")).map(String::trim).toArray(String[]::new); + } else { + parts = Arrays.stream(range.split(" ")).map(String::trim).toArray(String[]::new); + } + for (String part : parts) { + if (part.startsWith(">=") || part.startsWith("[")) { + versionStartIncluding = part.replace(">=", "").replace("[", "").trim(); + } else if (part.startsWith(">") || part.startsWith("(")) { + versionStartExcluding = part.replace(">", "").replace("(", "").trim(); + } else if (part.startsWith("<=") || part.endsWith("]")) { + versionEndIncluding = part.replace("<=", "").replace("]", "").trim(); + } else if (part.startsWith("<") || part.endsWith(")")) { + versionEndExcluding = part.replace("<", "").replace(")", "").trim(); + } else if (part.startsWith("=")) { + versionStartIncluding = part.replace("=", "").trim(); + versionEndIncluding = part.replace("=", "").trim(); + } else { + LOGGER.warn("Unable to determine version range " + part); + } + } + + VulnerableSoftware vs = qm.getVulnerableSoftwareByPurl(packageURL.getType(), packageURL.getNamespace(), packageURL.getName(), + versionEndExcluding, versionEndIncluding, versionStartExcluding, versionStartIncluding); + if (vs == null) { + vs = new VulnerableSoftware(); + vs.setVulnerable(true); + vs.setPurlType(packageURL.getType()); + vs.setPurlNamespace(packageURL.getNamespace()); + vs.setPurlName(packageURL.getName()); + vs.setVersion(packageURL.getVersion()); + vs.setVersionStartIncluding(versionStartIncluding); + vs.setVersionStartExcluding(versionStartExcluding); + vs.setVersionEndIncluding(versionEndIncluding); + vs.setVersionEndExcluding(versionEndExcluding); + } + vulnerableSoftwares.add(vs); + } + return vulnerableSoftwares; + } + + public String getSnykCvssConfig(ConfigPropertyConstants scannerSnykCvssSource) { + + try (QueryManager qm = new QueryManager()) { + final ConfigProperty property = qm.getConfigProperty( + scannerSnykCvssSource.getGroupName(), scannerSnykCvssSource.getPropertyName() + ); + if (property != null && ConfigProperty.PropertyType.STRING == property.getPropertyType()) { + return property.getPropertyValue(); + } + } + return scannerSnykCvssSource.getDefaultPropertyValue(); + } +} diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index b1cc3082da..2f042ed6f5 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -508,6 +508,11 @@ public synchronized VulnerabilityAlias synchronizeVulnerabilityAlias(Vulnerabili filter += "(osvId == :osvId || osvId == null)"; params.put("osvId", alias.getOsvId()); } + if (alias.getSnykId() != null) { + if (filter.length() > 0) filter += " && "; + filter += "(snykId == :snykId || snykId == null)"; + params.put("snykId", alias.getSnykId()); + } if (alias.getGsdId() != null) { if (filter.length() > 0) filter += " && "; filter += "(gsdId == :gsdId || gsdId == null)"; @@ -532,6 +537,7 @@ public synchronized VulnerabilityAlias synchronizeVulnerabilityAlias(Vulnerabili if (alias.getSonatypeId() != null) existingAlias.setSonatypeId(alias.getSonatypeId()); if (alias.getGhsaId() != null) existingAlias.setGhsaId(alias.getGhsaId()); if (alias.getOsvId() != null) existingAlias.setOsvId(alias.getOsvId()); + if (alias.getSnykId() != null) existingAlias.setSnykId(alias.getSnykId()); if (alias.getGsdId() != null) existingAlias.setGsdId(alias.getGsdId()); if (alias.getVulnDbId() != null) existingAlias.setVulnDbId(alias.getVulnDbId()); if (alias.getInternalId() != null) existingAlias.setInternalId(alias.getInternalId()); @@ -552,6 +558,8 @@ public List getVulnerabilityAliases(Vulnerability vulnerabil query = pm.newQuery(VulnerabilityAlias.class, "ghsaId == :ghsaId"); } else if (Vulnerability.Source.OSV.name().equals(vulnerability.getSource())) { query = pm.newQuery(VulnerabilityAlias.class, "osvId == :osvId"); + } else if (Vulnerability.Source.SNYK.name().equals(vulnerability.getSource())) { + query = pm.newQuery(VulnerabilityAlias.class, "snykId == :snykId"); } else if (Vulnerability.Source.VULNDB.name().equals(vulnerability.getSource())) { query = pm.newQuery(VulnerabilityAlias.class, "vulnDb == :vulnDb"); } else { diff --git a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java index ebd45b3298..034f7b0847 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ConfigPropertyResource.java @@ -18,8 +18,8 @@ */ package org.dependencytrack.resources.v1; -import alpine.common.logging.Logger; import alpine.model.ConfigProperty; +import alpine.model.IConfigProperty; import alpine.server.auth.PermissionRequired; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -50,8 +50,6 @@ @Api(value = "configProperty", authorizations = @Authorization(value = "X-Api-Key")) public class ConfigPropertyResource extends AbstractConfigPropertyResource { - private static final Logger LOGGER = Logger.getLogger(ConfigPropertyResource.class); - @GET @Produces(MediaType.APPLICATION_JSON) @ApiOperation( @@ -137,4 +135,6 @@ public Response updateConfigProperty(List list) { } return Response.ok(returnList).build(); } + + } diff --git a/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java index 195b6ad874..c601a8fe22 100644 --- a/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java @@ -28,6 +28,7 @@ import org.dependencytrack.event.ProjectMetricsUpdateEvent; import org.dependencytrack.event.VulnDbAnalysisEvent; import org.dependencytrack.event.VulnerabilityAnalysisEvent; +import org.dependencytrack.event.SnykAnalysisEvent; import org.dependencytrack.model.VulnerabilityAnalysisLevel; import org.dependencytrack.model.Component; import org.dependencytrack.model.Project; @@ -39,6 +40,7 @@ import org.dependencytrack.tasks.scanners.ScanTask; import org.dependencytrack.tasks.scanners.VulnDbAnalysisTask; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; +import org.dependencytrack.tasks.scanners.SnykAnalysisTask; import java.time.Duration; import java.time.Instant; @@ -54,6 +56,7 @@ public class VulnerabilityAnalysisTask implements Subscriber { private final List internalCandidates = new ArrayList<>(); private final List ossIndexCandidates = new ArrayList<>(); private final List vulnDbCandidates = new ArrayList<>(); + private final List snykCandidates = new ArrayList<>(); /** * {@inheritDoc} @@ -108,10 +111,12 @@ private void analyzeComponents(final QueryManager qm, final List comp final InternalAnalysisTask internalAnalysisTask = new InternalAnalysisTask(); final OssIndexAnalysisTask ossIndexAnalysisTask = new OssIndexAnalysisTask(); final VulnDbAnalysisTask vulnDbAnalysisTask = new VulnDbAnalysisTask(); + final SnykAnalysisTask snykAnalysisTask = new SnykAnalysisTask(); for (final Component component : components) { inspectComponentReadiness(component, internalAnalysisTask, internalCandidates); inspectComponentReadiness(component, ossIndexAnalysisTask, ossIndexCandidates); inspectComponentReadiness(component, vulnDbAnalysisTask, vulnDbCandidates); + inspectComponentReadiness(component, snykAnalysisTask, snykCandidates); } qm.detach(components); @@ -121,6 +126,7 @@ private void analyzeComponents(final QueryManager qm, final List comp // from interrupting the successful execution of all analyzers. performAnalysis(internalAnalysisTask, new InternalAnalysisEvent(internalCandidates), internalAnalysisTask.getAnalyzerIdentity(), event); performAnalysis(ossIndexAnalysisTask, new OssIndexAnalysisEvent(ossIndexCandidates), ossIndexAnalysisTask.getAnalyzerIdentity(), event); + performAnalysis(snykAnalysisTask, new SnykAnalysisEvent(snykCandidates), snykAnalysisTask.getAnalyzerIdentity(), event); performAnalysis(vulnDbAnalysisTask, new VulnDbAnalysisEvent(vulnDbCandidates), vulnDbAnalysisTask.getAnalyzerIdentity(), event); } diff --git a/src/main/java/org/dependencytrack/tasks/scanners/AnalyzerIdentity.java b/src/main/java/org/dependencytrack/tasks/scanners/AnalyzerIdentity.java index c90ad58843..b031d36ed6 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/AnalyzerIdentity.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/AnalyzerIdentity.java @@ -28,5 +28,6 @@ public enum AnalyzerIdentity { OSSINDEX_ANALYZER, NPM_AUDIT_ANALYZER, VULNDB_ANALYZER, + SNYK_ANALYZER, NONE } diff --git a/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java b/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java index 5e43e8bd91..62ef7f7a00 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java @@ -67,7 +67,7 @@ protected boolean isCacheCurrent(Vulnerability.Source source, String targetHost, try (QueryManager qm = new QueryManager()) { boolean isCacheCurrent = false; ConfigProperty cacheClearPeriod = qm.getConfigProperty(ConfigPropertyConstants.SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD.getGroupName(), ConfigPropertyConstants.SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD.getPropertyName()); - long cacheValidityPeriod = Long.valueOf(cacheClearPeriod.getPropertyValue()); + long cacheValidityPeriod = Long.parseLong(cacheClearPeriod.getPropertyValue()); ComponentAnalysisCache cac = qm.getComponentAnalysisCache(ComponentAnalysisCache.CacheType.VULNERABILITY, targetHost, source.name(), target); if (cac != null) { final Date now = new Date(); @@ -95,12 +95,15 @@ protected void applyAnalysisFromCache(Vulnerability.Source source, String target final JsonArray vulns = result.getJsonArray("vulnIds"); if (vulns != null) { for (JsonNumber vulnId : vulns.getValuesAs(JsonNumber.class)) { - final Vulnerability vulnerability = qm.getObjectById(Vulnerability.class, vulnId.longValue()); - final Component c = qm.getObjectByUuid(Component.class, component.getUuid()); - if (c == null) continue; - if (vulnerability != null) { - NotificationUtil.analyzeNotificationCriteria(qm, vulnerability, component, vulnerabilityAnalysisLevel); - qm.addVulnerability(vulnerability, c, analyzerIdentity); + final Vulnerability vulnerability; + if (vulnId.longValue() != 0) { + vulnerability = qm.getObjectById(Vulnerability.class, vulnId.longValue()); + final Component c = qm.getObjectByUuid(Component.class, component.getUuid()); + if (c == null) continue; + if (vulnerability != null) { + NotificationUtil.analyzeNotificationCriteria(qm, vulnerability, component, vulnerabilityAnalysisLevel); + qm.addVulnerability(vulnerability, c, analyzerIdentity); + } } } } @@ -109,7 +112,8 @@ protected void applyAnalysisFromCache(Vulnerability.Source source, String target } } - protected synchronized void updateAnalysisCacheStats(QueryManager qm, Vulnerability.Source source, String targetHost, String target, JsonObject result) { + protected synchronized void updateAnalysisCacheStats(QueryManager qm, Vulnerability.Source source, String + targetHost, String target, JsonObject result) { qm.updateComponentAnalysisCache(ComponentAnalysisCache.CacheType.VULNERABILITY, targetHost, source.name(), target, new Date(), result); } @@ -126,14 +130,15 @@ protected void addVulnerabilityToCache(Component component, Vulnerability vulner } } - protected void handleUnexpectedHttpResponse(final Logger logger, String url, final int statusCode, final String statusText) { + protected void handleUnexpectedHttpResponse(final Logger logger, String url, final int statusCode, + final String statusText) { logger.error("HTTP Status : " + statusCode + " " + statusText); logger.error(" - Analyzer URL : " + url); 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. URL: " + url + " HTTP Status: " + statusCode + ". Check log for details." ) + .content("An error occurred while communicating with a vulnerability intelligence source. URL: " + url + " HTTP Status: " + statusCode + ". Check log for details.") .level(NotificationLevel.ERROR) ); } diff --git a/src/main/java/org/dependencytrack/tasks/scanners/SnykAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/SnykAnalysisTask.java new file mode 100644 index 0000000000..e847ae1900 --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/scanners/SnykAnalysisTask.java @@ -0,0 +1,257 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.tasks.scanners; + +import alpine.common.logging.Logger; +import alpine.Config; +import org.dependencytrack.common.ConfigKey; +import alpine.event.framework.Event; +import alpine.event.framework.Subscriber; +import alpine.model.ConfigProperty; +import alpine.security.crypto.DataEncryption; +import kong.unirest.UnirestInstance; +import kong.unirest.GetRequest; +import kong.unirest.JsonNode; +import kong.unirest.UnirestException; +import kong.unirest.HttpResponse; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHeaders; +import org.dependencytrack.common.UnirestFactory; +import org.dependencytrack.event.IndexEvent; +import org.dependencytrack.event.SnykAnalysisEvent; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerabilityAnalysisLevel; +import org.dependencytrack.parser.snyk.SnykParser; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.NotificationUtil; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_SNYK_BASE_URL; + +/** + * Subscriber task that performs an analysis of component using Snyk vulnerability REST API. + */ +public class SnykAnalysisTask extends BaseComponentAnalyzerTask implements Subscriber { + + private String apiBaseUrl; + private static String apiEndPoint = "/issues?"; + + //number of threads to be used for snyk analyzer are configurable. Default is 10. Can be set based on user requirements. + private static final ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(Config.getInstance().getPropertyAsInt(ConfigKey.SNYK_THREAD_BATCH_SIZE)); + private static final Logger LOGGER = Logger.getLogger(SnykAnalysisTask.class); + private String apiToken; + private static int duration = 0; + private VulnerabilityAnalysisLevel vulnerabilityAnalysisLevel; + + public AnalyzerIdentity getAnalyzerIdentity() { + return AnalyzerIdentity.SNYK_ANALYZER; + } + + /** + * {@inheritDoc} + */ + public void inform(final Event e) { + Instant start = Instant.now(); + if (e instanceof SnykAnalysisEvent) { + if (!super.isEnabled(ConfigPropertyConstants.SCANNER_SNYK_ENABLED)) { + return; + } + try (QueryManager qm = new QueryManager()) { + final ConfigProperty apiTokenProperty = qm.getConfigProperty( + ConfigPropertyConstants.SCANNER_SNYK_API_TOKEN.getGroupName(), + ConfigPropertyConstants.SCANNER_SNYK_API_TOKEN.getPropertyName() + ); + final ConfigProperty orgIdProperty = qm.getConfigProperty( + ConfigPropertyConstants.SCANNER_SNYK_ORG_ID.getGroupName(), + ConfigPropertyConstants.SCANNER_SNYK_ORG_ID.getPropertyName() + ); + final ConfigProperty apiVersionProperty = qm.getConfigProperty( + ConfigPropertyConstants.SCANNER_SNYK_API_VERSION.getGroupName(), + ConfigPropertyConstants.SCANNER_SNYK_API_VERSION.getPropertyName() + ); + if (apiTokenProperty == null || apiTokenProperty.getPropertyValue() == null) { + LOGGER.error("Please provide API token for use with Snyk"); + return; + } + if (orgIdProperty == null || orgIdProperty.getPropertyValue() == null) { + LOGGER.error("Please provide organization ID to access Snyk"); + return; + } + if (apiVersionProperty == null || apiVersionProperty.getPropertyValue() == null) { + LOGGER.error("Please provide Snyk API version"); + return; + } + String baseUrl = qm.getConfigProperty( + SCANNER_SNYK_BASE_URL.getGroupName(), + SCANNER_SNYK_BASE_URL.getPropertyName()).getPropertyValue(); + if (baseUrl != null && baseUrl.endsWith("/")) { + baseUrl = StringUtils.chop(baseUrl); + } + try { + apiToken = "token " + DataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); + apiEndPoint += "version=" + apiVersionProperty.getPropertyValue().trim(); + String ORG_ID = orgIdProperty.getPropertyValue(); + apiBaseUrl = baseUrl + "/rest/orgs/" + ORG_ID + "/packages/"; + } catch (Exception ex) { + LOGGER.error("An error occurred decrypting the Snyk API Token. Skipping", ex); + return; + } + } + final SnykAnalysisEvent event = (SnykAnalysisEvent) e; + vulnerabilityAnalysisLevel = event.getVulnerabilityAnalysisLevel(); + LOGGER.info("Starting Snyk vulnerability analysis task"); + if (!event.getComponents().isEmpty()) { + analyze(event.getComponents()); + } + Instant end = Instant.now(); + LOGGER.info("Snyk vulnerability analysis complete"); + Duration timeElapsed = Duration.between(start, end); + LOGGER.info("Time taken to complete snyk vulnerability analysis task: " + timeElapsed.toMillis() + duration + " milliseconds"); + } + } + + /** + * Determines if the {@link SnykAnalysisTask} is capable of analyzing the specified Component. + * + * @param component the Component to analyze + * @return true if SnykAnalysisTask should analyze, false if not + */ + public boolean isCapable(final Component component) { + return component.getPurl() != null + && component.getPurl().getScheme() != null + && component.getPurl().getType() != null + && component.getPurl().getName() != null + && component.getPurl().getVersion() != null; + } + + /** + * Analyzes a list of Components. + * + * @param components a list of Components + */ + public void analyze(final List components) { + int trackComponent = 0; + final UnirestInstance ui = UnirestFactory.getUnirestInstance(); + CountDownLatch countDownLatch = new CountDownLatch(components.size()); + for (final Component component : components) { + if (trackComponent < components.size()) { + trackComponent += 1; + Runnable analysisUtil = () -> { + try { + final String snykUrl = apiBaseUrl + URLEncoder.encode(component.getPurlCoordinates().toString(), StandardCharsets.UTF_8) + apiEndPoint; + if (!isCacheCurrent(Vulnerability.Source.SNYK, apiBaseUrl, component.getPurl().toString())) { + try { + final GetRequest request = ui.get(snykUrl) + .header(HttpHeaders.AUTHORIZATION, this.apiToken); + final HttpResponse jsonResponse = request.asJson(); + if (jsonResponse.getStatus() == 200 || jsonResponse.getStatus() == 404) { + if (jsonResponse.getStatus() == 200) { + handle(component, jsonResponse.getBody().getObject(), jsonResponse.getStatus()); + } else if (jsonResponse.getStatus() == 404) { + handle(component, null, jsonResponse.getStatus()); + } + } else { + handleUnexpectedHttpResponse(LOGGER, apiBaseUrl, jsonResponse.getStatus(), jsonResponse.getStatusText()); + } + } catch (UnirestException e) { + handleRequestException(LOGGER, e); + } + } else { + LOGGER.debug("Cache is current, apply snyk analysis from cache"); + applyAnalysisFromCache(Vulnerability.Source.SNYK, apiBaseUrl, component.getPurl().toString(), component, getAnalyzerIdentity(), vulnerabilityAnalysisLevel); + } + } finally { + countDownLatch.countDown(); + } + }; + Instant startThread = Instant.now(); + executor.execute(analysisUtil); + + Instant endThread = Instant.now(); + duration += Duration.between(startThread, endThread).toMillis(); + } + } + try { + if (!countDownLatch.await(60, TimeUnit.MINUTES)) { + // Depending on the system load, it may take a while for the queued events + // to be processed. And depending on how large the projects are, it may take a + // while for the processing of the respective event to complete. + // It is unlikely though that either of these situations causes a block for + // over 60 minutes. If that happens, the system is under-resourced. + LOGGER.warn("The Analysis for project :" + components.get(0).getProject().getName() + "took longer than expected"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + + public void handle(Component component, JSONObject object, int responseCode) { + + try (QueryManager qm = new QueryManager()) { + if (responseCode == 200) { + String purl = null; + final JSONObject metaInfo = object.optJSONObject("meta"); + if (metaInfo != null) { + purl = metaInfo.optJSONObject("package").optString("url"); + if (purl == null) { + purl = component.getPurlCoordinates().toString(); + } + } + final JSONArray data = object.optJSONArray("data"); + if (data != null) { + SnykParser snykParser = new SnykParser(); + for (int count = 0; count < data.length(); count++) { + Vulnerability synchronizedVulnerability = snykParser.parse(data, qm, purl, count); + addVulnerabilityToCache(component, synchronizedVulnerability); + final Component componentPersisted = qm.getObjectByUuid(Component.class, component.getUuid()); + if (componentPersisted != null && synchronizedVulnerability.getVulnId() != null) { + NotificationUtil.analyzeNotificationCriteria(qm, synchronizedVulnerability, componentPersisted, vulnerabilityAnalysisLevel); + qm.addVulnerability(synchronizedVulnerability, componentPersisted, this.getAnalyzerIdentity()); + LOGGER.debug("Snyk vulnerability added : " + synchronizedVulnerability.getVulnId() + " to component " + component.getName()); + } + Event.dispatch(new IndexEvent(IndexEvent.Action.COMMIT, Vulnerability.class)); + } + } + if (component.getPurl() != null && apiBaseUrl != null) { + updateAnalysisCacheStats(qm, Vulnerability.Source.SNYK, apiBaseUrl, component.getPurl().toString(), component.getCacheResult()); + } + } else if (responseCode == 404) { + Vulnerability vulnerability = new Vulnerability(); + addVulnerabilityToCache(component, vulnerability); + if (component.getPurl() != null && apiBaseUrl != null) { + updateAnalysisCacheStats(qm, Vulnerability.Source.SNYK, apiBaseUrl, component.getPurl().toString(), component.getCacheResult()); + } + } + } + } +} diff --git a/src/test/java/org/dependencytrack/tasks/scanners/SnykAnalysisTaskTest.java b/src/test/java/org/dependencytrack/tasks/scanners/SnykAnalysisTaskTest.java new file mode 100644 index 0000000000..18d5896fd6 --- /dev/null +++ b/src/test/java/org/dependencytrack/tasks/scanners/SnykAnalysisTaskTest.java @@ -0,0 +1,218 @@ +package org.dependencytrack.tasks.scanners; + +import alpine.model.IConfigProperty; +import com.github.packageurl.PackageURL; +import kong.unirest.json.JSONArray; +import kong.unirest.json.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerableSoftware; +import org.dependencytrack.parser.snyk.SnykParser; +import org.dependencytrack.persistence.CweImporter; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_SNYK_CVSS_SOURCE; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_SNYK_API_TOKEN; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_SNYK_ENABLED; + +public class SnykAnalysisTaskTest extends PersistenceCapableTest { + + private JSONObject jsonObject; + + private SnykAnalysisTask task = new SnykAnalysisTask(); + + private SnykParser parser = new SnykParser(); + + @Test + public void testParseSnykJsonToAdvisoryAndSave() throws Exception { + new CweImporter().processCweDefinitions(); // Necessary for resolving CWEs + + prepareJsonObject("src/test/resources/unit/snyk.jsons/snyk-vuln.json"); + Component component = new Component(); + component.setPurl("pkg:npm/moment@2.24.0"); + component.setUuid(UUID.randomUUID()); + component.setName("test-snyk"); + + task.handle(component, jsonObject, 200); + + final Consumer assertVulnerability = (vulnerability) -> { + Assert.assertNotNull(vulnerability); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getTitle())); + Assert.assertFalse(StringUtils.isEmpty(vulnerability.getDescription())); + Assert.assertNotNull(vulnerability.getCwes()); + Assert.assertEquals(1, vulnerability.getCwes().size()); + Assert.assertEquals(1333, vulnerability.getCwes().get(0).intValue()); + Assert.assertEquals("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P", vulnerability.getCvssV3Vector()); + Assert.assertEquals(Severity.HIGH, vulnerability.getSeverity()); + Assert.assertNotNull(vulnerability.getCreated()); + Assert.assertNotNull(vulnerability.getAliases()); + Assert.assertEquals(2, vulnerability.getAliases().size()); + Assert.assertEquals("CVE-2022-31129", vulnerability.getAliases().get(1).getCveId()); + Assert.assertEquals("GHSA-wc69-rhjr-hc9g", vulnerability.getAliases().get(0).getGhsaId()); + }; + + Vulnerability vulnerability = qm.getVulnerabilityByVulnId("SNYK", "SNYK-JS-MOMENT-2944238", true); + assertVulnerability.accept(vulnerability); + + List vulnerableSoftware = qm.getAllVulnerableSoftwareByPurl(new PackageURL("pkg:npm/moment")); + Assert.assertEquals(2, vulnerableSoftware.size()); + Assert.assertEquals("2.18.0", vulnerableSoftware.get(0).getVersionStartIncluding()); + Assert.assertEquals("2.29.4", vulnerableSoftware.get(0).getVersionEndExcluding()); + } + + @Test + public void testParseVersionRanges() throws IOException { + + String jsonString = new String(Files.readAllBytes(Paths.get("src/test/resources/unit/snyk.jsons/ranges.json"))); + jsonObject = new JSONObject(jsonString); + JSONArray ranges = jsonObject.optJSONArray("ranges"); + String purl = "pkg:npm/bootstrap-table@1.20.0"; + List vulnerableSoftwares = parser.parseVersionRanges(qm, purl, ranges); + Assert.assertNotNull(vulnerableSoftwares); + Assert.assertEquals(3, vulnerableSoftwares.size()); + + VulnerableSoftware vs = vulnerableSoftwares.get(0); + Assert.assertEquals("npm", vs.getPurlType()); + Assert.assertEquals("bootstrap-table", vs.getPurlName()); + Assert.assertEquals("", vs.getVersionStartIncluding()); + Assert.assertEquals("2.12.6.1", vs.getVersionEndExcluding()); + + vs = vulnerableSoftwares.get(1); + Assert.assertEquals("2.13.0", vs.getVersionStartIncluding()); + Assert.assertEquals("2.13.2.1", vs.getVersionEndExcluding()); + + vs = vulnerableSoftwares.get(2); + Assert.assertEquals(null, vs.getVersionStartIncluding()); + Assert.assertEquals("1.20.2", vs.getVersionEndExcluding()); + } + + @Test + public void testParseSeveritiesNvd() throws IOException { + + // By default NVD is first priority for CVSS, no need to set config property. + String jsonString = new String(Files.readAllBytes(Paths.get("src/test/resources/unit/snyk.jsons/severities.json"))); + jsonObject = new JSONObject(jsonString); + JSONArray severities = jsonObject.optJSONArray("severities1"); + JSONObject cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("NVD", cvss.optString("source")); + + severities = jsonObject.optJSONArray("severities2"); + cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("SNYK", cvss.optString("source")); + + severities = jsonObject.optJSONArray("severities5"); + cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("RHEL", cvss.optString("source")); + } + + @Test + public void testParseSeveritiesSnyk() throws IOException { + + qm.createConfigProperty(SCANNER_SNYK_CVSS_SOURCE.getGroupName(), + SCANNER_SNYK_CVSS_SOURCE.getPropertyName(), + "SNYK", + IConfigProperty.PropertyType.STRING, + "First priority source for cvss calculation"); + + String jsonString = new String(Files.readAllBytes(Paths.get("src/test/resources/unit/snyk.jsons/severities.json"))); + jsonObject = new JSONObject(jsonString); + JSONArray severities = jsonObject.optJSONArray("severities1"); + JSONObject cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("SNYK", cvss.optString("source")); + + severities = jsonObject.optJSONArray("severities3"); + cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("NVD", cvss.optString("source")); + + severities = jsonObject.optJSONArray("severities4"); + cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("RHEL", cvss.optString("source")); + } + + @Test + public void testSelectCvssObjectBasedOnSource() throws IOException { + String jsonString = new String(Files.readAllBytes(Paths.get("src/test/resources/unit/snyk.jsons/severities.json"))); + jsonObject = new JSONObject(jsonString); + JSONArray severities = jsonObject.optJSONArray("severities1"); + JSONObject cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("NVD", cvss.optString("source")); + Assert.assertEquals("high", cvss.optString("level")); + Assert.assertEquals("7.5", cvss.optString("score")); + Assert.assertEquals("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", cvss.optString("vector")); + + severities = jsonObject.optJSONArray("severities4"); + cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("RHEL", cvss.optString("source")); + Assert.assertEquals("high", cvss.optString("level")); + Assert.assertEquals("7.5", cvss.optString("score")); + Assert.assertEquals("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", cvss.optString("vector")); + + severities = jsonObject.optJSONArray("severities2"); + cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("SNYK", cvss.optString("source")); + Assert.assertEquals("high", cvss.optString("level")); + Assert.assertEquals("7.5", cvss.optString("score")); + Assert.assertEquals("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P", cvss.optString("vector")); + severities = jsonObject.optJSONArray("severities3"); + cvss = parser.selectCvssObjectBasedOnSource(severities); + Assert.assertNotNull(cvss); + Assert.assertEquals("NVD", cvss.optString("source")); + Assert.assertEquals("high", cvss.optString("level")); + Assert.assertEquals("7.5", cvss.optString("score")); + Assert.assertEquals("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", cvss.optString("vector")); + } + + @Test + public void testGetSnykCvssConfig() { + qm.createConfigProperty(SCANNER_SNYK_API_TOKEN.getGroupName(), + SCANNER_SNYK_API_TOKEN.getPropertyName(), + "token", + IConfigProperty.PropertyType.STRING, + "token"); + qm.createConfigProperty(SCANNER_OSSINDEX_API_USERNAME.getGroupName(), + SCANNER_OSSINDEX_API_USERNAME.getPropertyName(), + "username", + IConfigProperty.PropertyType.STRING, + "username"); + + String config = parser.getSnykCvssConfig(SCANNER_SNYK_CVSS_SOURCE); + Assert.assertNotNull(config); + Assert.assertEquals("NVD", config); + config = parser.getSnykCvssConfig(SCANNER_SNYK_ENABLED); + Assert.assertNotNull(config); + Assert.assertEquals("false", config); + config = parser.getSnykCvssConfig(SCANNER_SNYK_API_TOKEN); + Assert.assertNotNull(config); + Assert.assertEquals("token", config); + config = parser.getSnykCvssConfig(SCANNER_OSSINDEX_API_USERNAME); + Assert.assertNotNull(config); + Assert.assertEquals("username", config); + } + + private void prepareJsonObject(String filePath) throws IOException { + // parse json file to Advisory object + String jsonString = new String(Files.readAllBytes(Paths.get(filePath))); + jsonObject = new JSONObject(jsonString); + } +} diff --git a/src/test/resources/unit/snyk.jsons/ranges.json b/src/test/resources/unit/snyk.jsons/ranges.json new file mode 100644 index 0000000000..e16d48d354 --- /dev/null +++ b/src/test/resources/unit/snyk.jsons/ranges.json @@ -0,0 +1,7 @@ +{ + "ranges": [ + "[, 2.12.6.1)", + "[2.13.0, 2.13.2.1)", + "<1.20.2" + ] +} \ No newline at end of file diff --git a/src/test/resources/unit/snyk.jsons/severities.json b/src/test/resources/unit/snyk.jsons/severities.json new file mode 100644 index 0000000000..e8cdfa8ea6 --- /dev/null +++ b/src/test/resources/unit/snyk.jsons/severities.json @@ -0,0 +1,78 @@ +{ + "severities1": [ + { + "source": "SNYK", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P" + }, + { + "source": "RHEL", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + { + "source": "NVD", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "severities2": [ + { + "source": "SNYK", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P" + }, + { + "source": "RHEL", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "severities3": [ + { + "source": "RHEL", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + { + "source": "NVD", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "severities4": [ + { + "source": "RHEL", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + { + "source": "SUSE", + "level": "high", + "score": null, + "vector": null + } + ], + "severities5": [ + { + "source": "RHEL", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + { + "source": "NVD", + "level": "high", + "score": null, + "vector": null + } + ] +} \ No newline at end of file diff --git a/src/test/resources/unit/snyk.jsons/snyk-vuln.json b/src/test/resources/unit/snyk.jsons/snyk-vuln.json new file mode 100644 index 0000000000..937916d0c7 --- /dev/null +++ b/src/test/resources/unit/snyk.jsons/snyk-vuln.json @@ -0,0 +1,147 @@ +{ + "jsonapi": { + "version": "1.0" + }, + "data": [ + { + "id": "SNYK-JS-MOMENT-2944238", + "type": "issue", + "attributes": { + "key": "SNYK-JS-MOMENT-2944238", + "title": "Regular Expression Denial of Service (ReDoS)", + "type": "package_vulnerability", + "created_at": "2022-07-07T06:05:46.236101Z", + "updated_at": "2022-07-07T07:33:16.298631Z", + "description": "## Overview\n[moment](https://www.npmjs.com/package/moment) is a lightweight JavaScript date library for parsing, validating, manipulating, and formatting dates.\n\nAffected versions of this package are vulnerable to Regular Expression Denial of Service (ReDoS) via the `preprocessRFC2822()` function in `from-string.js`, when processing a very long crafted string (over 10k characters).\r\n\r\n\r\n\r\n## PoC:\r\n```js\r\nmoment(\"(\".repeat(500000))\r\n```\n\n## Details\n\nDenial of Service (DoS) describes a family of attacks, all aimed at making a system inaccessible to its original and legitimate users. There are many types of DoS attacks, ranging from trying to clog the network pipes to the system by generating a large volume of traffic from many machines (a Distributed Denial of Service - DDoS - attack) to sending crafted requests that cause a system to crash or take a disproportional amount of time to process.\n\nThe Regular expression Denial of Service (ReDoS) is a type of Denial of Service attack. Regular expressions are incredibly powerful, but they aren't very intuitive and can ultimately end up making it easy for attackers to take your site down.\n\nLet’s take the following regular expression as an example:\n```js\nregex = /A(B|C+)+D/\n```\n\nThis regular expression accomplishes the following:\n- `A` The string must start with the letter 'A'\n- `(B|C+)+` The string must then follow the letter A with either the letter 'B' or some number of occurrences of the letter 'C' (the `+` matches one or more times). The `+` at the end of this section states that we can look for one or more matches of this section.\n- `D` Finally, we ensure this section of the string ends with a 'D'\n\nThe expression would match inputs such as `ABBD`, `ABCCCCD`, `ABCBCCCD` and `ACCCCCD`\n\nIt most cases, it doesn't take very long for a regex engine to find a match:\n\n```bash\n$ time node -e '/A(B|C+)+D/.test(\"ACCCCCCCCCCCCCCCCCCCCCCCCCCCCD\")'\n0.04s user 0.01s system 95% cpu 0.052 total\n\n$ time node -e '/A(B|C+)+D/.test(\"ACCCCCCCCCCCCCCCCCCCCCCCCCCCCX\")'\n1.79s user 0.02s system 99% cpu 1.812 total\n```\n\nThe entire process of testing it against a 30 characters long string takes around ~52ms. But when given an invalid string, it takes nearly two seconds to complete the test, over ten times as long as it took to test a valid string. The dramatic difference is due to the way regular expressions get evaluated.\n\nMost Regex engines will work very similarly (with minor differences). The engine will match the first possible way to accept the current character and proceed to the next one. If it then fails to match the next one, it will backtrack and see if there was another way to digest the previous character. If it goes too far down the rabbit hole only to find out the string doesn’t match in the end, and if many characters have multiple valid regex paths, the number of backtracking steps can become very large, resulting in what is known as _catastrophic backtracking_.\n\nLet's look at how our expression runs into this problem, using a shorter string: \"ACCCX\". While it seems fairly straightforward, there are still four different ways that the engine could match those three C's:\n1. CCC\n2. CC+C\n3. C+CC\n4. C+C+C.\n\nThe engine has to try each of those combinations to see if any of them potentially match against the expression. When you combine that with the other steps the engine must take, we can use [RegEx 101 debugger](https://regex101.com/debugger) to see the engine has to take a total of 38 steps before it can determine the string doesn't match.\n\nFrom there, the number of steps the engine must use to validate a string just continues to grow.\n\n| String | Number of C's | Number of steps |\n| -------|-------------:| -----:|\n| ACCCX | 3 | 38\n| ACCCCX | 4 | 71\n| ACCCCCX | 5 | 136\n| ACCCCCCCCCCCCCCX | 14 | 65,553\n\n\nBy the time the string includes 14 C's, the engine has to take over 65,000 steps just to see if the string is valid. These extreme situations can cause them to work very slowly (exponentially related to input size, as shown above), allowing an attacker to exploit this and can cause the service to excessively consume CPU, resulting in a Denial of Service.\n\n## Remediation\nUpgrade `moment` to version 2.29.4 or higher.\n## References\n- [GitHub Commit](https://github.com/moment/moment/commit/9a3b5894f3d5d602948ac8a02e4ee528a49ca3a3)\n- [GitHub Issue](https://github.com/moment/moment/issues/6012)\n- [GitHub PR](https://github.com/moment/moment/pull/6015)\n", + "problems": [ + { + "id": "CWE-1333", + "source": "CWE" + }, + { + "id": "GHSA-wc69-rhjr-hc9g", + "source": "GHSA" + }, + { + "id": "CVE-2022-31129", + "source": "CVE" + } + ], + "coordinates": [ + { + "remedies": [ + { + "type": "indeterminate", + "description": "Upgrade the package version to 2.29.4 to fix this vulnerability", + "details": { + "upgrade_package": "2.29.4" + } + } + ], + "representation": [ + ">=2.18.0 <2.29.4" + ] + } + ], + "severities": [ + { + "source": "CVE-2022-31129", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P" + } + ], + "severity": "high", + "slots": { + "disclosure_time": "2022-07-06T18:38:49Z", + "exploit": "Proof of Concept", + "publication_time": "2022-07-07T07:32:53Z", + "references": [ + { + "title": "GitHub Commit", + "url": "https://github.com/moment/moment/commit/9a3b5894f3d5d602948ac8a02e4ee528a49ca3a3" + }, + { + "title": "GitHub Issue", + "url": "https://github.com/moment/moment/issues/6012" + }, + { + "title": "GitHub PR", + "url": "https://github.com/moment/moment/pull/6015" + } + ] + } + } + }, + { + "id": "SNYK-JS-MOMENT-2440688", + "type": "issue", + "attributes": { + "key": "SNYK-JS-MOMENT-2440688", + "title": "Directory Traversal", + "type": "package_vulnerability", + "created_at": "2022-04-05T08:46:07.677588Z", + "updated_at": "2022-04-05T12:30:50.880408Z", + "description": "## Overview\n[moment](https://www.npmjs.com/package/moment) is a lightweight JavaScript date library for parsing, validating, manipulating, and formatting dates.\n\nAffected versions of this package are vulnerable to Directory Traversal when a user provides a locale string which is directly used to switch moment locale.\n\n## Details\n\nA Directory Traversal attack (also known as path traversal) aims to access files and directories that are stored outside the intended folder. By manipulating files with \"dot-dot-slash (../)\" sequences and its variations, or by using absolute file paths, it may be possible to access arbitrary files and directories stored on file system, including application source code, configuration, and other critical system files.\n\nDirectory Traversal vulnerabilities can be generally divided into two types:\n\n- **Information Disclosure**: Allows the attacker to gain information about the folder structure or read the contents of sensitive files on the system.\n\n`st` is a module for serving static files on web pages, and contains a [vulnerability of this type](https://snyk.io/vuln/npm:st:20140206). In our example, we will serve files from the `public` route.\n\nIf an attacker requests the following URL from our server, it will in turn leak the sensitive private key of the root user.\n\n```\ncurl http://localhost:8080/public/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/root/.ssh/id_rsa\n```\n**Note** `%2e` is the URL encoded version of `.` (dot).\n\n- **Writing arbitrary files**: Allows the attacker to create or replace existing files. This type of vulnerability is also known as `Zip-Slip`. \n\nOne way to achieve this is by using a malicious `zip` archive that holds path traversal filenames. When each filename in the zip archive gets concatenated to the target extraction folder, without validation, the final path ends up outside of the target folder. If an executable or a configuration file is overwritten with a file containing malicious code, the problem can turn into an arbitrary code execution issue quite easily.\n\nThe following is an example of a `zip` archive with one benign file and one malicious file. Extracting the malicious file will result in traversing out of the target folder, ending up in `/root/.ssh/` overwriting the `authorized_keys` file:\n\n```\n2018-04-15 22:04:29 ..... 19 19 good.txt\n2018-04-15 22:04:42 ..... 20 20 ../../../../../../root/.ssh/authorized_keys\n```\n\n## Remediation\nUpgrade `moment` to version 2.29.2 or higher.\n## References\n- [GitHub Commit](https://github.com/moment/moment/commit/4211bfc8f15746be4019bba557e29a7ba83d54c5)\n", + "problems": [ + { + "id": "CWE-22", + "source": "CWE" + }, + { + "id": "GHSA-8hfj-j24r-96c4", + "source": "GHSA" + }, + { + "id": "CVE-2022-24785", + "source": "CVE" + } + ], + "coordinates": [ + { + "remedies": [ + { + "type": "indeterminate", + "description": "Upgrade the package version to 2.29.2 to fix this vulnerability", + "details": { + "upgrade_package": "2.29.2" + } + } + ], + "representation": [ + "[0,]" + ] + } + ], + "severities": [ + { + "source": "CVE-2022-24785", + "level": "high", + "score": 7.5, + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + } + ], + "severity": "high", + "slots": { + "disclosure_time": "2022-04-05T08:39:23Z", + "exploit": "Not Defined", + "publication_time": "2022-04-05T12:30:50.878091Z", + "references": [ + { + "title": "GitHub Commit", + "url": "https://github.com/moment/moment/commit/4211bfc8f15746be4019bba557e29a7ba83d54c5" + } + ] + } + } + } + ], + "meta": { + "package": { + "name": "moment", + "type": "npm", + "url": "pkg:npm/moment@2.24.0", + "version": "2.24.0" + } + } +} \ No newline at end of file