diff --git a/vulntotal/__init__.py b/vulntotal/__init__.py
new file mode 100644
index 000000000..bdac1cd30
--- /dev/null
+++ b/vulntotal/__init__.py
@@ -0,0 +1,8 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# VulnerableCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/nexB/vulnerablecode for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
+#
diff --git a/vulntotal/datasources/osv.py b/vulntotal/datasources/osv.py
new file mode 100644
index 000000000..97ca7d38e
--- /dev/null
+++ b/vulntotal/datasources/osv.py
@@ -0,0 +1,140 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# VulnerableCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/nexB/vulnerablecode for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
+#
+
+import logging
+from typing import Iterable
+
+import requests
+from packageurl import PackageURL
+
+from vulntotal.ecosystem.nuget import get_closest_nuget_package_name
+from vulntotal.validator import DataSource
+from vulntotal.validator import VendorData
+from vulntotal.vulntotal_utils import get_item
+
+logger = logging.getLogger(__name__)
+
+
+class OSVDataSource(DataSource):
+    spdx_license_expression = "Apache-2.0"
+    license_url = "https://github.com/google/osv/blob/master/LICENSE"
+    url = "https://api.osv.dev/v1/query"
+
+    def fetch_advisory(self, payload):
+        """Fetch JSON advisory from OSV API for a given package payload """
+
+        response = requests.post(self.url, data=str(payload))
+        if not response.status_code == 200:
+            logger.error(f"Error while fetching {payload}: {response.status_code}")
+            return
+        return response.json()
+
+    def datasource_advisory(self, purl) -> Iterable[VendorData]:
+        payload = generate_payload(purl)
+        if not payload:
+            return
+        advisory = self.fetch_advisory(payload)
+        self._raw_dump.append(advisory)
+        return parse_advisory(advisory)
+
+    @classmethod
+    def supported_ecosystem(cls):
+        # source https://ossf.github.io/osv-schema/
+        return {
+            "npm": "npm",
+            "maven": "Maven",
+            "golang": "Go",
+            "nuget": "NuGet",
+            "pypi": "PyPI",
+            "rubygems": "RubyGems",
+            "crates.io": "crates.io",
+            "composer": "Packagist",
+            "linux": "Linux",
+            "oss-fuzz": "OSS-Fuzz",
+            "debian": "Debian",
+            "hex": "Hex",
+            "android": "Android",
+        }
+
+
+def parse_advisory(response) -> Iterable[VendorData]:
+    """Parse response from OSV API and yield VendorData"""
+
+    for vuln in response.get("vulns") or []:
+        aliases = []
+        affected_versions = []
+        fixed = []
+
+        aliases.extend(vuln.get("aliases") or [])
+        aliases.append(vuln.get("id")) if vuln.get("id") else None
+
+        try:
+            affected_versions.extend(get_item(vuln, "affected", 0, "versions") or [])
+        except:
+            pass
+
+        try:
+            for event in get_item(vuln, "affected", 0, "ranges", 0, "events") or []:
+                affected_versions.append(event.get("introduced")) if event.get(
+                    "introduced"
+                ) else None
+                fixed.append(event.get("fixed")) if event.get("fixed") else None
+        except:
+            pass
+
+        yield VendorData(
+            aliases=sorted(list(set(aliases))),
+            affected_versions=sorted(list(set(affected_versions))),
+            fixed_versions=sorted(list(set(fixed))),
+        )
+
+
+def generate_payload(purl):
+    """Generate compatible payload for OSV API from a PURL"""
+
+    supported_ecosystem = OSVDataSource.supported_ecosystem()
+    payload = {}
+    payload["version"] = purl.version
+    payload["package"] = {}
+
+    if purl.type in supported_ecosystem:
+        payload["package"]["ecosystem"] = supported_ecosystem[purl.type]
+
+    if purl.type == "maven":
+        if not purl.namespace:
+            logger.error(f"Invalid Maven PURL {str(purl)}")
+            return
+        payload["package"]["name"] = f"{purl.namespace}:{purl.name}"
+
+    elif purl.type == "packagist":
+        if not purl.namespace:
+            logger.error(f"Invalid Packagist PURL {str(purl)}")
+            return
+        payload["package"]["name"] = f"{purl.namespace}/{purl.name}"
+
+    elif purl.type == "linux":
+        if purl.name not in ("kernel", "Kernel"):
+            logger.error(f"Invalid Linux PURL {str(purl)}")
+            return
+        payload["package"]["name"] = "Kernel"
+
+    elif purl.type == "nuget":
+        nuget_package = get_closest_nuget_package_name(purl.name)
+        if not nuget_package:
+            logger.error(f"Invalid NuGet PURL {str(purl)}")
+            return
+        payload["package"]["name"] = nuget_package
+
+    elif purl.type == "golang" and purl.namespace:
+        payload["package"]["name"] = f"{purl.namespace}/{purl.name}"
+
+    else:
+        payload["package"]["name"] = purl.name
+
+    return payload
diff --git a/vulntotal/ecosystem/__init__.py b/vulntotal/ecosystem/__init__.py
new file mode 100644
index 000000000..bdac1cd30
--- /dev/null
+++ b/vulntotal/ecosystem/__init__.py
@@ -0,0 +1,8 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# VulnerableCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/nexB/vulnerablecode for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
+#
diff --git a/vulntotal/ecosystem/nuget.py b/vulntotal/ecosystem/nuget.py
new file mode 100644
index 000000000..1b459adc7
--- /dev/null
+++ b/vulntotal/ecosystem/nuget.py
@@ -0,0 +1,33 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# VulnerableCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/nexB/vulnerablecode for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
+#
+
+from urllib.parse import urljoin
+
+import requests
+
+
+def get_closest_nuget_package_name(query):
+    """
+    Return case-sensitive NuGet package name using
+    SearchQueryService provided by NuGet
+    """
+    url_nuget_service = "https://api.nuget.org/v3/index.json"
+    url_nuget_search = ""
+
+    api_resources = requests.get(url_nuget_service).json()
+    for resource in api_resources.get("resources") or []:
+        if resource.get("@type") == "SearchQueryService":
+            url_nuget_search = resource["@id"]
+            break
+
+    if url_nuget_search:
+        url_query = urljoin(url_nuget_search, f"?q={query}")
+        query_response = requests.get(url_query).json()
+        if query_response.get("data"):
+            return query_response["data"][0]["id"]
diff --git a/vulntotal/tests/test_data/osv/advisory.txt b/vulntotal/tests/test_data/osv/advisory.txt
new file mode 100644
index 000000000..e4e42849b
--- /dev/null
+++ b/vulntotal/tests/test_data/osv/advisory.txt
@@ -0,0 +1 @@
+{"vulns":[{"id":"PYSEC-2014-8","details":"The default configuration for bccache.FileSystemBytecodeCache in Jinja2 before 2.7.2 does not properly create temporary files, which allows local users to gain privileges via a crafted .cache file with a name starting with __jinja2_ in /tmp.","aliases":["CVE-2014-1402"],"modified":"2021-07-05T00:01:22.043149Z","published":"2014-05-19T14:55:00Z","references":[{"type":"WEB","url":"https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=734747"},{"type":"ADVISORY","url":"http://advisories.mageia.org/MGASA-2014-0028.html"},{"type":"WEB","url":"http://jinja.pocoo.org/docs/changelog/"},{"type":"WEB","url":"http://openwall.com/lists/oss-security/2014/01/10/3"},{"type":"REPORT","url":"https://bugzilla.redhat.com/show_bug.cgi?id=1051421"},{"type":"ADVISORY","url":"http://www.mandriva.com/security/advisories?name=MDVSA-2014:096"},{"type":"WEB","url":"http://openwall.com/lists/oss-security/2014/01/10/2"},{"type":"ADVISORY","url":"http://secunia.com/advisories/59017"},{"type":"ADVISORY","url":"http://secunia.com/advisories/58918"},{"type":"ADVISORY","url":"http://secunia.com/advisories/60770"},{"type":"ADVISORY","url":"http://secunia.com/advisories/60738"},{"type":"ADVISORY","url":"http://secunia.com/advisories/56287"},{"type":"WEB","url":"http://www.gentoo.org/security/en/glsa/glsa-201408-13.xml"},{"type":"WEB","url":"https://oss.oracle.com/pipermail/el-errata/2014-June/004192.html"},{"type":"ADVISORY","url":"http://secunia.com/advisories/58783"},{"type":"ADVISORY","url":"http://rhn.redhat.com/errata/RHSA-2014-0748.html"},{"type":"ADVISORY","url":"http://rhn.redhat.com/errata/RHSA-2014-0747.html"}],"affected":[{"package":{"name":"jinja2","ecosystem":"PyPI","purl":"pkg:pypi/jinja2"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.7.2"}]}],"versions":["2.0","2.0rc1","2.1","2.1.1","2.2","2.2.1","2.3","2.3.1","2.4","2.4.1","2.5","2.5.1","2.5.2","2.5.3","2.5.4","2.5.5","2.6","2.7","2.7.1"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/jinja2/PYSEC-2014-8.yaml"}}],"schema_version":"1.2.0"},{"id":"PYSEC-2014-82","details":"FileSystemBytecodeCache in Jinja2 2.7.2 does not properly create temporary directories, which allows local users to gain privileges by pre-creating a temporary directory with a user's uid.  NOTE: this vulnerability exists because of an incomplete fix for CVE-2014-1402.","aliases":["CVE-2014-0012"],"modified":"2021-08-27T03:22:05.027573Z","published":"2014-05-19T14:55:00Z","references":[{"type":"REPORT","url":"https://bugzilla.redhat.com/show_bug.cgi?id=1051421"},{"type":"WEB","url":"https://github.com/mitsuhiko/jinja2/pull/292"},{"type":"FIX","url":"https://github.com/mitsuhiko/jinja2/commit/acb672b6a179567632e032f547582f30fa2f4aa7"},{"type":"WEB","url":"http://seclists.org/oss-sec/2014/q1/73"},{"type":"WEB","url":"https://github.com/mitsuhiko/jinja2/pull/296"},{"type":"ADVISORY","url":"http://secunia.com/advisories/60738"},{"type":"WEB","url":"http://www.gentoo.org/security/en/glsa/glsa-201408-13.xml"},{"type":"ADVISORY","url":"http://secunia.com/advisories/56328"}],"affected":[{"package":{"name":"jinja2","ecosystem":"PyPI","purl":"pkg:pypi/jinja2"},"ranges":[{"type":"GIT","repo":"https://github.com/mitsuhiko/jinja2","events":[{"introduced":"0"},{"fixed":"acb672b6a179567632e032f547582f30fa2f4aa7"}]},{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.7.3"}]}],"versions":["2.0","2.0rc1","2.1","2.1.1","2.2","2.2.1","2.3","2.3.1","2.4","2.4.1","2.5","2.5.1","2.5.2","2.5.3","2.5.4","2.5.5","2.6","2.7","2.7.1","2.7.2"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/jinja2/PYSEC-2014-82.yaml"}}],"schema_version":"1.2.0"},{"id":"PYSEC-2019-217","details":"In Pallets Jinja before 2.10.1, str.format_map allows a sandbox escape.","aliases":["CVE-2019-10906","GHSA-462w-v97r-4m45"],"modified":"2021-11-22T04:57:52.862665Z","published":"2019-04-07T00:29:00Z","references":[{"type":"ARTICLE","url":"https://palletsprojects.com/blog/jinja-2-10-1-released"},{"type":"WEB","url":"https://lists.apache.org/thread.html/b2380d147b508bbcb90d2cad443c159e63e12555966ab4f320ee22da@%3Ccommits.airflow.apache.org%3E"},{"type":"WEB","url":"https://lists.apache.org/thread.html/46c055e173b52d599c648a98199972dbd6a89d2b4c4647b0500f2284@%3Cdevnull.infra.apache.org%3E"},{"type":"WEB","url":"https://lists.apache.org/thread.html/f0c4a03418bcfe70c539c5dbaf99c04c98da13bfa1d3266f08564316@%3Ccommits.airflow.apache.org%3E"},{"type":"WEB","url":"https://lists.apache.org/thread.html/7f39f01392d320dfb48e4901db68daeece62fd60ef20955966739993@%3Ccommits.airflow.apache.org%3E"},{"type":"WEB","url":"https://lists.apache.org/thread.html/57673a78c4d5c870d3f21465c7e2946b9f8285c7c57e54c2ae552f02@%3Ccommits.airflow.apache.org%3E"},{"type":"WEB","url":"https://lists.apache.org/thread.html/320441dccbd9a545320f5f07306d711d4bbd31ba43dc9eebcfc602df@%3Cdevnull.infra.apache.org%3E"},{"type":"WEB","url":"https://lists.apache.org/thread.html/2b52b9c8b9d6366a4f1b407a8bde6af28d9fc73fdb3b37695fd0d9ac@%3Cdevnull.infra.apache.org%3E"},{"type":"WEB","url":"https://lists.apache.org/thread.html/09fc842ff444cd43d9d4c510756fec625ef8eb1175f14fd21de2605f@%3Cdevnull.infra.apache.org%3E"},{"type":"WEB","url":"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QCDYIS254EJMBNWOG4S5QY6AOTOR4TZU/"},{"type":"WEB","url":"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DSW3QZMFVVR7YE3UT4YRQA272TYAL5AF/"},{"type":"WEB","url":"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/TS7IVZAJBWOHNRDMFJDIZVFCMRP6YIUQ/"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2019:1152"},{"type":"WEB","url":"http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00030.html"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2019:1237"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2019:1329"},{"type":"WEB","url":"https://usn.ubuntu.com/4011-1/"},{"type":"WEB","url":"https://usn.ubuntu.com/4011-2/"},{"type":"WEB","url":"http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00064.html"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-462w-v97r-4m45"}],"affected":[{"package":{"name":"jinja2","ecosystem":"PyPI","purl":"pkg:pypi/jinja2"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.10.1"}]}],"versions":["2.0","2.0rc1","2.1","2.1.1","2.10","2.2","2.2.1","2.3","2.3.1","2.4","2.4.1","2.5","2.5.1","2.5.2","2.5.3","2.5.4","2.5.5","2.6","2.7","2.7.1","2.7.2","2.7.3","2.8","2.8.1","2.9","2.9.1","2.9.2","2.9.3","2.9.4","2.9.5","2.9.6"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/jinja2/PYSEC-2019-217.yaml"}}],"schema_version":"1.2.0"},{"id":"PYSEC-2019-220","details":"In Pallets Jinja before 2.8.1, str.format allows a sandbox escape.","aliases":["CVE-2016-10745","GHSA-hj2j-77xm-mc5v"],"modified":"2021-11-22T04:57:52.929678Z","published":"2019-04-08T13:29:00Z","references":[{"type":"ARTICLE","url":"https://palletsprojects.com/blog/jinja-281-released/"},{"type":"FIX","url":"https://github.com/pallets/jinja/commit/9b53045c34e61013dc8f09b7e52a555fa16bed16"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2019:1022"},{"type":"WEB","url":"http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00030.html"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2019:1237"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2019:1260"},{"type":"WEB","url":"https://usn.ubuntu.com/4011-1/"},{"type":"WEB","url":"https://usn.ubuntu.com/4011-2/"},{"type":"WEB","url":"http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00064.html"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2019:3964"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2019:4062"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-hj2j-77xm-mc5v"}],"affected":[{"package":{"name":"jinja2","ecosystem":"PyPI","purl":"pkg:pypi/jinja2"},"ranges":[{"type":"GIT","repo":"https://github.com/pallets/jinja","events":[{"introduced":"0"},{"fixed":"9b53045c34e61013dc8f09b7e52a555fa16bed16"}]},{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.8.1"}]}],"versions":["2.0","2.0rc1","2.1","2.1.1","2.2","2.2.1","2.3","2.3.1","2.4","2.4.1","2.5","2.5.1","2.5.2","2.5.3","2.5.4","2.5.5","2.6","2.7","2.7.1","2.7.2","2.7.3","2.8"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/jinja2/PYSEC-2019-220.yaml"}}],"schema_version":"1.2.0"},{"id":"PYSEC-2021-66","details":"This affects the package jinja2 from 0.0.0 and before 2.11.3. The ReDoS vulnerability is mainly due to the `_punctuation_re regex` operator and its use of multiple wildcards. The last wildcard is the most exploitable as it searches for trailing punctuation. This issue can be mitigated by Markdown to format user content instead of the urlize filter, or by implementing request timeouts and limiting process memory.","aliases":["CVE-2020-28493","SNYK-PYTHON-JINJA2-1012994","GHSA-g3rq-g295-4j3m"],"modified":"2021-03-22T16:34:00Z","published":"2021-02-01T20:15:00Z","references":[{"type":"WEB","url":"https://github.com/pallets/jinja/blob/ab81fd9c277900c85da0c322a2ff9d68a235b2e6/src/jinja2/utils.py%23L20"},{"type":"WEB","url":"https://github.com/pallets/jinja/pull/1343"},{"type":"ADVISORY","url":"https://snyk.io/vuln/SNYK-PYTHON-JINJA2-1012994"},{"type":"WEB","url":"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/PVAKCOO7VBVUBM3Q6CBBTPBFNP5NDXF4/"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-g3rq-g295-4j3m"}],"affected":[{"package":{"name":"jinja2","ecosystem":"PyPI","purl":"pkg:pypi/jinja2"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.11.3"}]}],"versions":["2.0rc1","2.0","2.1","2.1.1","2.2","2.2.1","2.3","2.3.1","2.4","2.4.1","2.5","2.5.1","2.5.2","2.5.3","2.5.4","2.5.5","2.6","2.7","2.7.1","2.7.2","2.7.3","2.8","2.8.1","2.9","2.9.1","2.9.2","2.9.3","2.9.4","2.9.5","2.9.6","2.10","2.10.1","2.10.2","2.10.3","2.11.0","2.11.1","2.11.2"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/jinja2/PYSEC-2021-66.yaml"}}],"schema_version":"1.2.0"}]}
\ No newline at end of file
diff --git a/vulntotal/tests/test_data/osv/parse_advisory_data-expected.json b/vulntotal/tests/test_data/osv/parse_advisory_data-expected.json
new file mode 100644
index 000000000..c7bfc490c
--- /dev/null
+++ b/vulntotal/tests/test_data/osv/parse_advisory_data-expected.json
@@ -0,0 +1,195 @@
+[
+  {
+    "affected_versions": [
+      "0",
+      "2.0",
+      "2.0rc1",
+      "2.1",
+      "2.1.1",
+      "2.2",
+      "2.2.1",
+      "2.3",
+      "2.3.1",
+      "2.4",
+      "2.4.1",
+      "2.5",
+      "2.5.1",
+      "2.5.2",
+      "2.5.3",
+      "2.5.4",
+      "2.5.5",
+      "2.6",
+      "2.7",
+      "2.7.1"
+    ],
+    "fixed_versions": [
+      "2.7.2"
+    ],
+    "aliases": [
+      "CVE-2014-1402",
+      "PYSEC-2014-8"
+    ]
+  },
+  {
+    "affected_versions": [
+      "0",
+      "2.0",
+      "2.0rc1",
+      "2.1",
+      "2.1.1",
+      "2.2",
+      "2.2.1",
+      "2.3",
+      "2.3.1",
+      "2.4",
+      "2.4.1",
+      "2.5",
+      "2.5.1",
+      "2.5.2",
+      "2.5.3",
+      "2.5.4",
+      "2.5.5",
+      "2.6",
+      "2.7",
+      "2.7.1",
+      "2.7.2"
+    ],
+    "fixed_versions": [
+      "acb672b6a179567632e032f547582f30fa2f4aa7"
+    ],
+    "aliases": [
+      "CVE-2014-0012",
+      "PYSEC-2014-82"
+    ]
+  },
+  {
+    "affected_versions": [
+      "0",
+      "2.0",
+      "2.0rc1",
+      "2.1",
+      "2.1.1",
+      "2.10",
+      "2.2",
+      "2.2.1",
+      "2.3",
+      "2.3.1",
+      "2.4",
+      "2.4.1",
+      "2.5",
+      "2.5.1",
+      "2.5.2",
+      "2.5.3",
+      "2.5.4",
+      "2.5.5",
+      "2.6",
+      "2.7",
+      "2.7.1",
+      "2.7.2",
+      "2.7.3",
+      "2.8",
+      "2.8.1",
+      "2.9",
+      "2.9.1",
+      "2.9.2",
+      "2.9.3",
+      "2.9.4",
+      "2.9.5",
+      "2.9.6"
+    ],
+    "fixed_versions": [
+      "2.10.1"
+    ],
+    "aliases": [
+      "CVE-2019-10906",
+      "GHSA-462w-v97r-4m45",
+      "PYSEC-2019-217"
+    ]
+  },
+  {
+    "affected_versions": [
+      "0",
+      "2.0",
+      "2.0rc1",
+      "2.1",
+      "2.1.1",
+      "2.2",
+      "2.2.1",
+      "2.3",
+      "2.3.1",
+      "2.4",
+      "2.4.1",
+      "2.5",
+      "2.5.1",
+      "2.5.2",
+      "2.5.3",
+      "2.5.4",
+      "2.5.5",
+      "2.6",
+      "2.7",
+      "2.7.1",
+      "2.7.2",
+      "2.7.3",
+      "2.8"
+    ],
+    "fixed_versions": [
+      "9b53045c34e61013dc8f09b7e52a555fa16bed16"
+    ],
+    "aliases": [
+      "CVE-2016-10745",
+      "GHSA-hj2j-77xm-mc5v",
+      "PYSEC-2019-220"
+    ]
+  },
+  {
+    "affected_versions": [
+      "0",
+      "2.0",
+      "2.0rc1",
+      "2.1",
+      "2.1.1",
+      "2.10",
+      "2.10.1",
+      "2.10.2",
+      "2.10.3",
+      "2.11.0",
+      "2.11.1",
+      "2.11.2",
+      "2.2",
+      "2.2.1",
+      "2.3",
+      "2.3.1",
+      "2.4",
+      "2.4.1",
+      "2.5",
+      "2.5.1",
+      "2.5.2",
+      "2.5.3",
+      "2.5.4",
+      "2.5.5",
+      "2.6",
+      "2.7",
+      "2.7.1",
+      "2.7.2",
+      "2.7.3",
+      "2.8",
+      "2.8.1",
+      "2.9",
+      "2.9.1",
+      "2.9.2",
+      "2.9.3",
+      "2.9.4",
+      "2.9.5",
+      "2.9.6"
+    ],
+    "fixed_versions": [
+      "2.11.3"
+    ],
+    "aliases": [
+      "CVE-2020-28493",
+      "GHSA-g3rq-g295-4j3m",
+      "PYSEC-2021-66",
+      "SNYK-PYTHON-JINJA2-1012994"
+    ]
+  }
+]
\ No newline at end of file
diff --git a/vulntotal/tests/test_data/osv/payloads_data-expected.json b/vulntotal/tests/test_data/osv/payloads_data-expected.json
new file mode 100644
index 000000000..2710217c3
--- /dev/null
+++ b/vulntotal/tests/test_data/osv/payloads_data-expected.json
@@ -0,0 +1,70 @@
+[
+  {
+    "version": "2.4.1",
+    "package": {
+      "ecosystem": "PyPI",
+      "name": "jinja2"
+    }
+  },
+  {
+    "version": "10",
+    "package": {
+      "ecosystem": "Android",
+      "name": "System"
+    }
+  },
+  {
+    "version": "1.1.3-1",
+    "package": {
+      "name": "davical"
+    }
+  },
+  {
+    "version": "10.1.0-M8",
+    "package": {
+      "ecosystem": "Maven",
+      "name": "org.apache.tomcat:tomcat"
+    }
+  },
+  {
+    "version": "v5.4.195",
+    "package": {
+      "ecosystem": "Linux",
+      "name": "Kernel"
+    }
+  },
+  {
+    "version": "12.0.5",
+    "package": {
+      "name": "dolibarr/dolibarr"
+    }
+  },
+  {
+    "version": "0.9.7",
+    "package": {
+      "ecosystem": "crates.io",
+      "name": "sha2"
+    }
+  },
+  {
+    "version": "2.18.0",
+    "package": {
+      "ecosystem": "NuGet",
+      "name": "Moment.js"
+    }
+  },
+  {
+    "version": "3.1.3",
+    "package": {
+      "ecosystem": "npm",
+      "name": "semver-regex"
+    }
+  },
+  {
+    "version": "1.1.0",
+    "package": {
+      "ecosystem": "Go",
+      "name": "github.com/cloudflare/cfrpki"
+    }
+  }
+]
\ No newline at end of file
diff --git a/vulntotal/tests/test_data/osv/purls.txt b/vulntotal/tests/test_data/osv/purls.txt
new file mode 100644
index 000000000..d911f1539
--- /dev/null
+++ b/vulntotal/tests/test_data/osv/purls.txt
@@ -0,0 +1,10 @@
+pkg:pypi/jinja2@2.4.1
+pkg:android/System@10
+pkg:debian:8/davical@1.1.3-1
+pkg:maven/org.apache.tomcat/tomcat@10.1.0-M8
+pkg:linux/Kernel@v5.4.195
+pkg:packagist/dolibarr/dolibarr@12.0.5
+pkg:crates.io/sha2@0.9.7
+pkg:nuget/Moment.js@2.18.0
+pkg:npm/semver-regex@3.1.3
+pkg:golang/github.com/cloudflare/cfrpki@1.1.0
\ No newline at end of file
diff --git a/vulntotal/tests/test_osv.py b/vulntotal/tests/test_osv.py
new file mode 100644
index 000000000..96c8bd24e
--- /dev/null
+++ b/vulntotal/tests/test_osv.py
@@ -0,0 +1,37 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# VulnerableCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/nexB/vulnerablecode for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
+#
+
+import json
+from pathlib import Path
+
+from commoncode import testcase
+from packageurl import PackageURL
+
+from vulnerabilities.tests import util_tests
+from vulntotal.datasources import osv
+
+
+class TestOSV(testcase.FileBasedTesting):
+    test_data_dir = str(Path(__file__).resolve().parent / "test_data" / "osv")
+
+    def test_generate_payload(self):
+        file_purls = self.get_test_loc("purls.txt")
+        with open(file_purls) as f:
+            purls = f.readlines()
+        results = [osv.generate_payload(PackageURL.from_string(purl)) for purl in purls]
+        expected_file = self.get_test_loc("payloads_data-expected.json", must_exist=False)
+        util_tests.check_results_against_json(results, expected_file)
+
+    def test_parse_advisory(self):
+        advisory_page = self.get_test_loc("advisory.txt")
+        with open(advisory_page) as f:
+            advisory = json.load(f)
+        results = [adv.to_dict() for adv in osv.parse_advisory(advisory)]
+        expected_file = self.get_test_loc("parse_advisory_data-expected.json", must_exist=False)
+        util_tests.check_results_against_json(results, expected_file)
diff --git a/vulntotal/vulntotal_utils.py b/vulntotal/vulntotal_utils.py
new file mode 100644
index 000000000..bb422c99b
--- /dev/null
+++ b/vulntotal/vulntotal_utils.py
@@ -0,0 +1,190 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# VulnerableCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/nexB/vulnerablecode for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
+#
+
+import operator
+from typing import Union
+
+
+class GenericVersion:
+    def __init__(self, version):
+        self.value = version.replace(" ", "").lstrip("v")
+
+        self.decomposed = tuple(
+            [int(com) if com.isnumeric() else com for com in self.value.split(".")]
+        )
+
+    def __str__(self):
+        return str(self.value)
+
+    def __eq__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        return self.value.__eq__(other.value)
+
+    def __lt__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        for i, j in zip(self.decomposed, other.decomposed):
+            if not isinstance(i, type(j)):
+                continue
+            if i.__gt__(j):
+                return False
+        return True
+
+    def __le__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        return self.__lt__(other) or self.__eq__(other)
+
+
+def compare(version, package_comparator, package_version):
+    operator_comparator = {
+        "<": operator.lt,
+        ">": operator.gt,
+        "=": operator.eq,
+        "<=": operator.le,
+        ">=": operator.ge,
+        "==": operator.eq,
+        "!=": operator.ne,
+        ")": operator.lt,
+        "]": operator.le,
+        "(": operator.gt,
+        "[": operator.ge,
+    }
+    compare = operator_comparator[package_comparator]
+    return compare(version, package_version)
+
+
+def parse_constraint(constraint):
+    """
+    Return operator and version from a constraint
+    For example:
+    >>> assert parse_constraint(">=7.0.0") == ('>=', '7.0.0')
+    >>> assert parse_constraint("=7.0.0") == ('=', '7.0.0')
+    >>> assert parse_constraint("[3.0.0") == ('[', '3.0.0')
+    >>> assert parse_constraint("3.1.25]") == (']', '3.1.25')
+    """
+    if constraint.startswith(("<=", ">=", "==", "!=")):
+        return constraint[:2], constraint[2:]
+
+    if constraint.startswith(("<", ">", "=", "[", "(")):
+        return constraint[0], constraint[1:]
+
+    if constraint.endswith(("]", ")")):
+        return constraint[-1], constraint[:-1]
+
+
+def github_constraints_satisfied(github_constrain, version):
+    """
+    Return True or False depending on whether the given version satisfies the github constrain
+    For example:
+    >>> assert github_constraints_satisfied(">= 7.0.0, <= 7.6.57", "7.1.1") == True
+    >>> assert github_constraints_satisfied(">= 10.4.0, <= 10.4.1", "10.6.0") == False
+    """
+    gh_constraints = github_constrain.strip().replace(" ", "")
+    constraints = gh_constraints.split(",")
+    for constraint in constraints:
+        gh_comparator, gh_version = parse_constraint(constraint)
+        if not gh_version:
+            continue
+        if not compare(GenericVersion(version), gh_comparator, GenericVersion(gh_version)):
+            return False
+    return True
+
+
+def snky_constraints_satisfied(snyk_constrain, version):
+    """
+    Return True or False depending on whether the given version satisfies the snyk constrain
+    For example:
+    >>> assert snky_constraints_satisfied(">=4.0.0, <4.0.10.16", "4.0.10.15") == True
+    >>> assert snky_constraints_satisfied(" >=4.1.0, <4.4.15.7", "4.0.10.15") == False
+    >>> assert snky_constraints_satisfied("[3.0.0,3.1.25)", "3.0.2") == True
+    """
+    snyk_constraints = snyk_constrain.strip().replace(" ", "")
+    constraints = snyk_constraints.split(",")
+    for constraint in constraints:
+        snyk_comparator, snyk_version = parse_constraint(constraint)
+        if not snyk_version:
+            continue
+        if not compare(GenericVersion(version), snyk_comparator, GenericVersion(snyk_version)):
+            return False
+    return True
+
+
+def gitlab_constraints_satisfied(gitlab_constrain, version):
+    """
+    Return True or False depending on whether the given version satisfies the gitlab constrain
+    For example:
+    >>> assert gitlab_constraints_satisfied("[7.0.0,7.0.11),[7.2.0,7.2.4)", "7.2.1") == True
+    >>> assert gitlab_constraints_satisfied("[7.0.0,7.0.11),[7.2.0,7.2.4)", "8.2.1") == False
+    >>> assert gitlab_constraints_satisfied( ">=4.0,<4.3||>=5.0,<5.2", "5.4") == False
+    >>> assert gitlab_constraints_satisfied( ">=0.19.0 <0.30.0", "0.24") == True
+    >>> assert gitlab_constraints_satisfied( ">=1.5,<1.5.2", "2.2") == False
+    """
+
+    gitlab_constraints = gitlab_constrain.strip()
+    if gitlab_constraints.startswith(("[", "(")):
+        # transform "[7.0.0,7.0.11),[7.2.0,7.2.4)" -> [ "[7.0.0,7.0.11)", "[7.2.0,7.2.4)" ]
+        splitted = gitlab_constraints.split(",")
+        constraints = [f"{a},{b}" for a, b in zip(splitted[::2], splitted[1::2])]
+        delimiter = ","
+
+    else:
+        # transform ">=4.0,<4.3||>=5.0,<5.2" -> [ ">=4.0,<4.3", ">=5.0,<5.2" ]
+        # transform ">=0.19.0 <0.30.0" -> [ ">=0.19.0 <0.30.0" ]
+        # transform ">=1.5,<1.5.2" -> [ ">=1.5,<1.5.2" ]
+        delimiter = "," if "," in gitlab_constraints else " "
+        constraints = gitlab_constraints.split("||")
+
+    for constraint in constraints:
+        is_constraint_satisfied = True
+        for subcontraint in constraint.strip().split(delimiter):
+            gitlab_comparator, gitlab_version = parse_constraint(subcontraint.strip())
+            if not gitlab_version:
+                continue
+            if not compare(
+                GenericVersion(version), gitlab_comparator, GenericVersion(gitlab_version)
+            ):
+                is_constraint_satisfied = False
+                break
+
+        if is_constraint_satisfied:
+            return True
+    return False
+
+
+def get_item(entity: Union[dict, list], *attributes):
+    """
+    Return `item` by going through all the `attributes` present in the `dictionary/list`
+
+    Do a DFS for the `item` in the `dictionary/list` by traversing the `attributes`
+    and return None if can not traverse through the `attributes`
+    For example:
+    >>> get_item({'a': {'b': {'c': 'd'}}}, 'a', 'b', 'e')
+    Traceback (most recent call last):
+        ...
+    KeyError: "Missing attribute e in {'c': 'd'}"
+    >>> assert get_item({'a': {'b': {'c': 'd'}}}, 'a', 'b', 'c') == 'd'
+    >>> assert get_item({'a': [{'b': {'c': 'd'}}]}, 'a', 0, 'b') == {'c': 'd'}
+    >>> assert get_item(['b', ['c', ['d']]], 1, 1, 0) == 'd'
+    """
+    for attribute in attributes:
+        if not entity:
+            return
+        if not isinstance(entity, (dict, list)):
+            raise TypeError(f"Entity must be of type `dict` or `list` not {type(entity)}")
+        if isinstance(entity, dict) and attribute not in entity:
+            raise KeyError(f"Missing attribute {attribute} in {entity}")
+        if isinstance(entity, list) and not isinstance(attribute, int):
+            raise TypeError(f"List indices must be integers not {type(attribute)}")
+        if isinstance(entity, list) and len(entity) <= attribute:
+            raise IndexError(f"Index {attribute} out of range for {entity}")
+
+        entity = entity[attribute]
+    return entity
