diff --git a/pom.xml b/pom.xml index e9230a37d6..adeff877d6 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,8 @@ 11.1.2.jre11-preview 8.0.29 42.4.0 + + 6.0.0 application false @@ -269,6 +271,12 @@ commons-compress 1.21 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${lib.jackson-databind.version} + junit @@ -330,6 +338,31 @@ + + org.openapitools + openapi-generator-maven-plugin + ${plugin.openapi-generator.version} + + + + generate + + + ${project.basedir}/src/main/resources/osv_service_v1.swagger.json + java + false + false + + src/gen/java/main + apache-httpclient + com.google.osv.api + com.google.osv.model + jackson + + + + + maven-antrun-plugin 3.1.0 diff --git a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index e41c2c493a..219904cb74 100644 --- a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -43,6 +43,7 @@ import org.dependencytrack.tasks.repositories.RepositoryMetaAnalyzerTask; import org.dependencytrack.tasks.scanners.InternalAnalysisTask; import org.dependencytrack.tasks.scanners.OssIndexAnalysisTask; +import org.dependencytrack.tasks.scanners.OsvAnalysisTask; import org.dependencytrack.tasks.scanners.VulnDbAnalysisTask; import javax.servlet.ServletContextEvent; @@ -80,6 +81,7 @@ public void contextInitialized(final ServletContextEvent event) { EVENT_SERVICE.subscribe(InternalAnalysisEvent.class, InternalAnalysisTask.class); EVENT_SERVICE.subscribe(OssIndexAnalysisEvent.class, OssIndexAnalysisTask.class); EVENT_SERVICE.subscribe(GitHubAdvisoryMirrorEvent.class, GitHubAdvisoryMirrorTask.class); + EVENT_SERVICE.subscribe(OsvAnalysisEvent.class, OsvAnalysisTask.class); EVENT_SERVICE.subscribe(VulnDbSyncEvent.class, VulnDbSyncTask.class); EVENT_SERVICE.subscribe(VulnDbAnalysisEvent.class, VulnDbAnalysisTask.class); EVENT_SERVICE.subscribe(VulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class); @@ -114,6 +116,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(OsvAnalysisTask.class); EVENT_SERVICE.unsubscribe(VulnDbSyncTask.class); EVENT_SERVICE.unsubscribe(VulnDbAnalysisTask.class); EVENT_SERVICE.unsubscribe(VulnerabilityAnalysisTask.class); diff --git a/src/main/java/org/dependencytrack/event/OsvAnalysisEvent.java b/src/main/java/org/dependencytrack/event/OsvAnalysisEvent.java new file mode 100644 index 0000000000..9acb299d36 --- /dev/null +++ b/src/main/java/org/dependencytrack/event/OsvAnalysisEvent.java @@ -0,0 +1,43 @@ +/* + * 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.event; + +import org.dependencytrack.model.Component; + +import java.util.List; + +/** + * Defines an event used to start an analysis via Sonatype OSS Index REST API. + * + * @author Steve Springett + * @since 3.2.0 + */ +public class OsvAnalysisEvent extends VulnerabilityAnalysisEvent { + + public OsvAnalysisEvent() { } + + public OsvAnalysisEvent(final Component component) { + super(component); + } + + public OsvAnalysisEvent(final List components) { + super(components); + } + +} diff --git a/src/main/java/org/dependencytrack/model/Vulnerability.java b/src/main/java/org/dependencytrack/model/Vulnerability.java index 486332a9c0..f17c40d080 100644 --- a/src/main/java/org/dependencytrack/model/Vulnerability.java +++ b/src/main/java/org/dependencytrack/model/Vulnerability.java @@ -95,7 +95,8 @@ public enum Source { VULNDB, // VulnDB from Risk Based Security OSSINDEX, // Sonatype OSS Index RETIREJS, // Retire.js - INTERNAL // Internally-managed (and manually entered) vulnerability + INTERNAL, // Internally-managed (and manually entered) vulnerability + OSV } @PrimaryKey diff --git a/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java index 4d1fb20d66..d260b95fff 100644 --- a/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java @@ -25,6 +25,7 @@ import org.dependencytrack.event.InternalAnalysisEvent; import org.dependencytrack.event.MetricsUpdateEvent; import org.dependencytrack.event.OssIndexAnalysisEvent; +import org.dependencytrack.event.OsvAnalysisEvent; import org.dependencytrack.event.PortfolioVulnerabilityAnalysisEvent; import org.dependencytrack.event.VulnDbAnalysisEvent; import org.dependencytrack.event.VulnerabilityAnalysisEvent; @@ -35,6 +36,7 @@ import org.dependencytrack.tasks.scanners.CacheableScanTask; import org.dependencytrack.tasks.scanners.InternalAnalysisTask; import org.dependencytrack.tasks.scanners.OssIndexAnalysisTask; +import org.dependencytrack.tasks.scanners.OsvAnalysisTask; import org.dependencytrack.tasks.scanners.ScanTask; import org.dependencytrack.tasks.scanners.VulnDbAnalysisTask; @@ -49,6 +51,7 @@ public class VulnerabilityAnalysisTask implements Subscriber { private final List internalCandidates = new ArrayList<>(); private final List ossIndexCandidates = new ArrayList<>(); + private final List osvCandidates = new ArrayList<>(); private final List vulnDbCandidates = new ArrayList<>(); /** @@ -103,10 +106,12 @@ private void analyzeComponents(final QueryManager qm, final List comp */ final InternalAnalysisTask internalAnalysisTask = new InternalAnalysisTask(); final OssIndexAnalysisTask ossIndexAnalysisTask = new OssIndexAnalysisTask(); + final var osvAnalysisTask = new OsvAnalysisTask(); final VulnDbAnalysisTask vulnDbAnalysisTask = new VulnDbAnalysisTask(); for (final Component component : components) { inspectComponentReadiness(component, internalAnalysisTask, internalCandidates); inspectComponentReadiness(component, ossIndexAnalysisTask, ossIndexCandidates); + inspectComponentReadiness(component, osvAnalysisTask, osvCandidates); inspectComponentReadiness(component, vulnDbAnalysisTask, vulnDbCandidates); } @@ -117,6 +122,7 @@ private void analyzeComponents(final QueryManager qm, final List comp // from interrupting the successful execution of all analyzers. performAnalysis(internalAnalysisTask, new InternalAnalysisEvent(internalCandidates)); performAnalysis(ossIndexAnalysisTask, new OssIndexAnalysisEvent(ossIndexCandidates)); + performAnalysis(osvAnalysisTask, new OsvAnalysisEvent(osvCandidates)); performAnalysis(vulnDbAnalysisTask, new VulnDbAnalysisEvent(vulnDbCandidates)); } diff --git a/src/main/java/org/dependencytrack/tasks/scanners/AnalyzerIdentity.java b/src/main/java/org/dependencytrack/tasks/scanners/AnalyzerIdentity.java index c90ad58843..7c23a26a9e 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, + OSV_ANALYZER, NONE } diff --git a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java index 61ffa909e9..e074393db6 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java @@ -80,7 +80,7 @@ public AnalyzerIdentity getAnalyzerIdentity() { */ public void inform(final Event e) { if (e instanceof OssIndexAnalysisEvent) { - if (!super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED)) { + if (super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED)) { return; } try (QueryManager qm = new QueryManager()) { diff --git a/src/main/java/org/dependencytrack/tasks/scanners/OsvAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/OsvAnalysisTask.java new file mode 100644 index 0000000000..b39868cc22 --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/scanners/OsvAnalysisTask.java @@ -0,0 +1,195 @@ +package org.dependencytrack.tasks.scanners; + +import alpine.common.logging.Logger; +import alpine.event.framework.Event; +import alpine.event.framework.Subscriber; +import com.google.osv.ApiClient; +import com.google.osv.ApiException; +import com.google.osv.api.ApiApi; +import com.google.osv.model.OsvVulnerability; +import com.google.osv.model.V1BatchQuery; +import com.google.osv.model.V1BatchVulnerabilityList; +import com.google.osv.model.V1Query; +import com.google.osv.model.V1QueryPackage; +import com.google.osv.model.V1VulnerabilityList; +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.common.HttpClientPool; +import org.dependencytrack.event.OsvAnalysisEvent; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.NotificationUtil; + +import java.util.List; +import java.util.Optional; + +public class OsvAnalysisTask extends BaseComponentAnalyzerTask implements Subscriber { + + private static final Logger LOGGER = Logger.getLogger(OsvAnalysisTask.class); + + @Override + public void inform(final Event e) { + if (!(e instanceof OsvAnalysisEvent)) { + return; + } + + final var analysisEvent = (OsvAnalysisEvent) e; + if (!analysisEvent.getComponents().isEmpty()) { + analyze(analysisEvent.getComponents()); + } + } + + @Override + public AnalyzerIdentity getAnalyzerIdentity() { + return AnalyzerIdentity.OSV_ANALYZER; + } + + @Override + public void analyze(final List components) { + final var osvApiClient = new ApiClient(HttpClientPool.getClient()); + final var osvApi = new ApiApi(osvApiClient); + + // TODO: Limit batch size to <= 1000 + final var batchQuery = new V1BatchQuery(); + for (final Component component : components) { + batchQuery.addQueriesItem(new V1Query() + ._package(new V1QueryPackage().purl(component.getPurl().toString()))); + } + + final List vulnerabilityLists; + try { + final V1BatchVulnerabilityList queryResult = osvApi.oSVQueryAffectedBatch(batchQuery); + + vulnerabilityLists = queryResult.getResults(); + if (vulnerabilityLists == null || vulnerabilityLists.isEmpty()) { + LOGGER.warn("OSV returned no results"); + return; + } + } catch (ApiException ex) { + LOGGER.error("Performing batch query failed", ex); + return; + } + + // OSV will return a list of vulnerabilities for each query in the batch we submitted. + // If no vulnerabilities are found, the respective list will be empty. + // Correlation between input queries / components and results is based on their order. + if (vulnerabilityLists.size() != components.size()) { + LOGGER.warn("OSV returned " + vulnerabilityLists.size() + " results, but we expected " + components.size()); + return; + } + + for (int i = 0; i < components.size(); i++) { + final V1VulnerabilityList vulnerabilityList = vulnerabilityLists.get(i); + if (vulnerabilityList.getVulns() == null || vulnerabilityList.getVulns().isEmpty()) { + LOGGER.info("No vulnerabilities found for component " + components.get(i).getPurl()); + continue; + } + + try (final var qm = new QueryManager()) { + final var component = qm.getObjectById(Component.class, components.get(i).getId()); + + for (OsvVulnerability osvVuln : vulnerabilityList.getVulns()) { + // Do we know this vulnerability already? + Vulnerability vuln = resolveVulnerability(qm, osvVuln.getId()); + + if (vuln == null) { + try { + // Vulnerabilities in batch query responses only contain their respective ID. + // If we need more fields, we have to explicitly request them. + // TODO: Cache these responses? + osvVuln = osvApi.oSVGetVulnById(osvVuln.getId()); + } catch (ApiException e) { + LOGGER.error("Requesting details for vulnerability " + osvVuln.getId() + " failed", e); + continue; + } + + // Primary ID of the vulnerability is unknown, but maybe we know one of its aliases? + if (osvVuln.getAliases() != null && !osvVuln.getAliases().isEmpty()) { + for (final String alias : osvVuln.getAliases()) { + vuln = resolveVulnerability(qm, alias); + if (vuln != null) { + break; + } + } + } + + // Vulnerability is not known to us yet, so we have to create it. + if (vuln == null) { + vuln = new Vulnerability(); + + // Similar to how it's done in OssIndexAnalysisTask, we prefer using the ID + // of the authoritative vulnerability source. We also prioritize CVE/NVD over GHSA. + // Vulnerability data will ultimately be overridden by those sources once + // Dependency-Track starts mirroring them. + final Optional optCve = resolveCve(osvVuln); + final Optional optGhsaId = resolveGhsaId(osvVuln); + if (optCve.isPresent()) { + vuln.setSource(Vulnerability.Source.NVD); + vuln.setVulnId(optCve.get()); + } else if (optGhsaId.isPresent()) { + vuln.setSource(Vulnerability.Source.GITHUB); + vuln.setVulnId(optGhsaId.get()); + } else { + vuln.setSource(Vulnerability.Source.OSV); + vuln.setVulnId(osvVuln.getId()); + } + + vuln.setTitle(StringUtils.truncate(osvVuln.getSummary(), 255)); + vuln.setDescription(osvVuln.getDetails()); + + // TODO: Parse more details + + vuln = qm.createVulnerability(vuln, false); + } + } + + LOGGER.info(osvVuln.getId() + " matched to " + vuln.getVulnId() + " (" + vuln.getSource() + ")"); + NotificationUtil.analyzeNotificationCriteria(qm, vuln, component); + qm.addVulnerability(vuln, component, this.getAnalyzerIdentity(), osvVuln.getId(), "https://osv.dev/vulnerability/" + osvVuln.getId()); + } + } + } + } + + @Override + public boolean isCapable(final Component component) { + return component.getPurl() != null + && component.getPurl().getName() != null + && component.getPurl().getVersion() != null; + } + + private Vulnerability resolveVulnerability(final QueryManager qm, final String vulnerabilityId) { + return qm.getVulnerabilityByVulnId(resolveVulnerabilitySource(vulnerabilityId), vulnerabilityId); + } + + private Vulnerability.Source resolveVulnerabilitySource(final String vulnerabilityId) { + if (StringUtils.startsWith(vulnerabilityId, "CVE-")) { + return Vulnerability.Source.NVD; + } else if (StringUtils.startsWith(vulnerabilityId, "GHSA-")) { + return Vulnerability.Source.GITHUB; + } + + return Vulnerability.Source.OSV; + } + + private Optional resolveCve(final OsvVulnerability osvVuln) { + if (resolveVulnerabilitySource(osvVuln.getId()) == Vulnerability.Source.NVD) { + return Optional.ofNullable(osvVuln.getId()); + } + + return Optional.ofNullable(osvVuln.getAliases()).orElseGet(List::of).stream() + .filter(alias -> resolveVulnerabilitySource(alias) == Vulnerability.Source.NVD) + .findFirst(); + } + + private Optional resolveGhsaId(final OsvVulnerability osvVuln) { + if (resolveVulnerabilitySource(osvVuln.getId()) == Vulnerability.Source.GITHUB) { + return Optional.ofNullable(osvVuln.getId()); + } + + return Optional.ofNullable(osvVuln.getAliases()).orElseGet(List::of).stream() + .filter(alias -> resolveVulnerabilitySource(alias) == Vulnerability.Source.GITHUB) + .findFirst(); + } + +} diff --git a/src/main/resources/osv_service_v1.swagger.json b/src/main/resources/osv_service_v1.swagger.json new file mode 100644 index 0000000000..0baf29e1bc --- /dev/null +++ b/src/main/resources/osv_service_v1.swagger.json @@ -0,0 +1,514 @@ +{ + "swagger": "2.0", + "info": { + "title": "OSV", + "version": "1.0", + "description": "# OSV API\n---\n\n### How does the API work?\n\nThe API currently accepts a git commit hash or a `(package, version number)` and returns the\nlist of vulnerabilities that are present for that version.\n\n# Getting Started\n---\n\n### Using the API\n\nBrowse the rest of the documentation for details on the\nAPI.\n\nFor some quick examples, run:\n\n```\ncurl -X POST -d '{\"commit\": \"6879efc2c1596d11a6a6ad296f80063b558d5e0f\"}' \\\n \"https://api.osv.dev/v1/query\"\n```\n\n```\ncurl -X POST -d \\\n '{\"version\": \"2.4.1\", \"package\": {\"name\": \"jinja2\", \"ecosystem\": \"PyPI\"}}' \\\n \"https://api.osv.dev/v1/query\"\n```\n" + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1/query": { + "post": { + "summary": "Query vulnerabilities for a particular project at a given commit or\nversion.", + "operationId": "OSV_QueryAffected", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1VulnerabilityList" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1Query" + } + } + ], + "tags": [ + "api" + ], + "x-code-samples": [ + { + "lang": "Curl example", + "source": "curl -X POST -d \\\n '{\"commit\": \"6879efc2c1596d11a6a6ad296f80063b558d5e0f\"}' \\\n \"https://api.osv.dev/v1/query\"\n\ncurl -X POST -d \\\n '{\"package\": {\"name\": \"mruby\"}, \"version\": \"2.1.2rc\"}' \\\n \"https://api.osv.dev/v1/query\"" + } + ] + } + }, + "/v1/querybatch": { + "post": { + "summary": "Query vulnerabilities (batched) for given package versions and commits.\nThis currently allows a maximum of 1000 package versions to be included in a single query.", + "operationId": "OSV_QueryAffectedBatch", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1BatchVulnerabilityList" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1BatchQuery" + } + } + ], + "tags": [ + "api" + ], + "x-code-samples": [ + { + "lang": "Curl example", + "source": "cat <." + }, + "purl": { + "type": "string", + "description": "Optional. The package URL for this package." + } + }, + "description": "Package information and version." + }, + "osvRange": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/osvRangeType", + "description": "Required. The type of version information." + }, + "repo": { + "type": "string", + "description": "Required if type is GIT. The publicly accessible URL of the repo that can\nbe directly passed to clone commands." + }, + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/osvEvent" + }, + "description": "Required. Version event information." + } + }, + "description": "Affected ranges." + }, + "osvRangeType": { + "type": "string", + "enum": [ + "UNSPECIFIED", + "GIT", + "SEMVER", + "ECOSYSTEM" + ], + "default": "UNSPECIFIED", + "description": "Type of the version information." + }, + "osvReference": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/osvReferenceType", + "description": "Required. The type of the reference." + }, + "url": { + "type": "string", + "description": "Required. The URL." + } + }, + "description": "Reference URL." + }, + "osvReferenceType": { + "type": "string", + "enum": [ + "NONE", + "WEB", + "ADVISORY", + "REPORT", + "FIX", + "PACKAGE", + "ARTICLE" + ], + "default": "NONE" + }, + "osvSeverity": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/osvSeverityType", + "description": "The type of this severity entry." + }, + "score": { + "type": "string", + "description": "The quantitative score." + } + } + }, + "osvSeverityType": { + "type": "string", + "enum": [ + "UNSPECIFIED", + "CVSS_V3" + ], + "default": "UNSPECIFIED", + "description": "Type of the severity." + }, + "osvVulnerability": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "description": "The OSV schema version." + }, + "id": { + "type": "string", + "description": "The `id` field is a unique identifier for the vulnerability entry. It is a\nstring of the format `-`, where `DB` names the database and\n`ENTRYID` is in the format used by the database. For example:\n\u201cOSV-2020-111\u201d, \u201cCVE-2021-3114\u201d, or \u201cGHSA-vp9c-fpxx-744v\u201d." + }, + "published": { + "type": "string", + "format": "date-time", + "description": "The RFC3339 timestamp indicating when this entry was published." + }, + "modified": { + "type": "string", + "format": "date-time", + "description": "The RFC3339 timestamp indicating when this entry was last modified." + }, + "withdrawn": { + "type": "string", + "format": "date-time", + "description": "Optional. The RFC3339 timestamp indicating when this entry is considered to\nbe withdrawn." + }, + "aliases": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional. IDs for the same vulnerability in other databases." + }, + "related": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional. List of IDs of closely related vulnerabilities, such as the same\nproblem in alternate ecosystems." + }, + "package": { + "description": "Required. Package information.", + "allOf": [ + { + "$ref": "#/definitions/osvPackage" + } + ] + }, + "summary": { + "type": "string", + "description": "Required. One line human readable summary for the vulnerability. It is\nrecommended to keep this under 120 characters." + }, + "details": { + "type": "string", + "description": "Required. Any additional human readable details for the vulnerability." + }, + "affected": { + "type": "array", + "items": { + "$ref": "#/definitions/osvAffected" + }, + "description": "Required. Affected commit ranges and versions." + }, + "references": { + "type": "array", + "items": { + "$ref": "#/definitions/osvReference" + }, + "description": "Optional. URLs to more information/advisories (including the\nscheme e.g \"https://\")." + }, + "severity": { + "type": "array", + "items": { + "$ref": "#/definitions/osvSeverity" + }, + "description": "Optional. Severity of the vulnerability." + }, + "credits": { + "type": "array", + "items": { + "$ref": "#/definitions/osvCredit" + }, + "description": "Optional. Credits for the vulnerability." + }, + "database_specific": { + "type": "object", + "description": "Optional. JSON object holding additional information about the\nvulnerability as defined by the database for which the record applies." + } + }, + "description": "A vulnerability entry.\nThe protobuf representation is *NOT* stable and only used for implementing\nthe JSON based API." + }, + "protobufAny": { + "type": "object", + "properties": { + "typeUrl": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + }, + "protobufNullValue": { + "type": "string", + "enum": [ + "NULL_VALUE" + ], + "default": "NULL_VALUE", + "description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value." + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1BatchQuery": { + "type": "object", + "properties": { + "queries": { + "type": "array", + "items": { + "$ref": "#/definitions/v1Query" + }, + "description": "The queries that form this batch query." + } + }, + "description": "Batch query format." + }, + "v1BatchVulnerabilityList": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/v1VulnerabilityList" + } + } + }, + "description": "Batched lists of Vulnerability entries." + }, + "v1Query": { + "type": "object", + "properties": { + "commit": { + "type": "string", + "description": "The commit hash to query for. If specified, `version` should not be set." + }, + "version": { + "type": "string", + "description": "The version string to query for. A fuzzy match is done against upstream\nversions. If specified, `commit` should not be set." + }, + "package": { + "description": "The package to query against. When a `commit` hash is given, this is\noptional.", + "allOf": [ + { + "$ref": "#/definitions/osvPackage" + } + ] + } + }, + "description": "Query format." + }, + "v1VulnerabilityList": { + "type": "object", + "properties": { + "vulns": { + "type": "array", + "items": { + "$ref": "#/definitions/osvVulnerability" + } + } + }, + "description": "A list of Vulnerability entries." + } + }, + "host": "api.osv.dev", + "schemes": ["https"], + "tags": [ + { + "name": "api", + "x-displayName": "API", + "description": "The API has 3 methods:" + }, + { + "name": "vulnerability_schema", + "x-displayName": "Vulnerability schema", + "description": "Please see the [OpenSSF Open Source Vulnerability spec](https://ossf.github.io/osv-schema/)." + } + ], + "x-tagGroups": [ + { + "name": "API", + "tags": [ + "api" + ] + }, + { + "name": "Schema", + "tags": [ + "vulnerability_schema" + ] + } + ] +} \ No newline at end of file