Repository: google/osv.dev

Issue:
Support for packageurl
Support to identify package with package url would be nice and it will be easy to integrate with other tool chain.
https://github.com/package-url/purl-spec


Discussion:
I could not agree more!
Hi!

Could I ask where you are suggesting to add support for purls? As part of an API query? 

Our thinking with our current schema and API is to keep it compatible with PURLs where we can, i.e. keeping the "ecosystem" values consistent with the ones chosen by purl ("golang", "pypi" etc), so that in most cases you can just split the PURL into the ecosystem name and the package path and version and still have it accepted by OSV.


That is correct. But if purl will be supported in api query then it will be easily integrated with other open source tools like owasp dependency track, nexb vulnerable code

Resolving patch:
diff --git a/gcp/api/server.py b/gcp/api/server.py
--- a/gcp/api/server.py
+++ b/gcp/api/server.py
@@ -77,9 +77,11 @@ def QueryAffected(self, request, context):
     if request.query.HasField('package'):
       package_name = request.query.package.name
       ecosystem = request.query.package.ecosystem
+      purl = request.query.package.purl
     else:
       package_name = ''
       ecosystem = ''
+      purl = ''
 
     if request.query.WhichOneof('param') == 'commit':
       bugs = query_by_commit(request.query.commit, to_response=bug_to_response)
@@ -87,6 +89,7 @@ def QueryAffected(self, request, context):
       bugs = query_by_version(
           package_name,
           ecosystem,
+          purl,
           request.query.version,
           to_response=bug_to_response)
     else:
@@ -131,7 +134,8 @@ def query_by_commit(commit, to_response=bug_to_response):
   return _get_bugs(bug_ids, to_response=to_response)
 
 
-def _is_semver_affected(affected_packages, package_name, ecosystem, version):
+def _is_semver_affected(affected_packages, package_name, ecosystem, purl,
+                        version):
   """Returns whether or not the given version is within an affected SEMVER
   range."""
   version = semver_index.parse(version)
@@ -144,6 +148,9 @@ def _is_semver_affected(affected_packages, package_name, ecosystem, version):
     if ecosystem and ecosystem != affected_package.package.ecosystem:
       continue
 
+    if purl and purl != affected_package.package.purl:
+      continue
+
     for affected_range in affected_package.ranges:
       if affected_range.type != 'SEMVER':
         continue
@@ -160,7 +167,7 @@ def _is_semver_affected(affected_packages, package_name, ecosystem, version):
   return affected
 
 
-def _query_by_semver(query, package_name, ecosystem, version):
+def _query_by_semver(query, package_name, ecosystem, purl, version):
   """Query by semver."""
   if not semver_index.is_valid(version):
     return []
@@ -170,7 +177,7 @@ def _query_by_semver(query, package_name, ecosystem, version):
 
   return [
       bug for bug in query if _is_semver_affected(
-          bug.affected_packages, package_name, ecosystem, version)
+          bug.affected_packages, package_name, ecosystem, purl, version)
   ]
 
 
@@ -188,12 +195,21 @@ def _query_by_generic_version(base_query, version):
   return list(query)
 
 
-def query_by_version(project, ecosystem, version, to_response=bug_to_response):
+def query_by_version(project,
+                     ecosystem,
+                     purl,
+                     version,
+                     to_response=bug_to_response):
   """Query by (fuzzy) version."""
   ecosystem_info = ecosystems.get(ecosystem)
   is_semver = ecosystem_info and ecosystem_info.is_semver
-  query = osv.Bug.query(osv.Bug.status == osv.BugStatus.PROCESSED,
-                        osv.Bug.project == project, osv.Bug.public == True)  # pylint: disable=singleton-comparison
+  if project:
+    query = osv.Bug.query(osv.Bug.status == osv.BugStatus.PROCESSED,
+                          osv.Bug.project == project, osv.Bug.public == True)  # pylint: disable=singleton-comparison
+  elif purl:
+    query = osv.Bug.query(osv.Bug.status == osv.BugStatus.PROCESSED,
+                          osv.Bug.purl == purl, osv.Bug.public == True)  # pylint: disable=singleton-comparison
+
   if ecosystem:
     query = query.filter(osv.Bug.ecosystem == ecosystem)
 
@@ -201,12 +217,12 @@ def query_by_version(project, ecosystem, version, to_response=bug_to_response):
   if ecosystem:
     if is_semver:
       # Ecosystem supports semver only.
-      bugs.extend(_query_by_semver(query, project, ecosystem, version))
+      bugs.extend(_query_by_semver(query, project, ecosystem, purl, version))
     else:
       bugs.extend(_query_by_generic_version(query, version))
   else:
     # Unspecified ecosystem. Try both.
-    bugs.extend(_query_by_semver(query, project, ecosystem, version))
+    bugs.extend(_query_by_semver(query, project, ecosystem, purl, version))
     bugs.extend(_query_by_generic_version(query, version))
 
   return [to_response(bug) for bug in bugs]


Test patch:
diff --git a/gcp/api/integration_tests.py b/gcp/api/integration_tests.py
--- a/gcp/api/integration_tests.py
+++ b/gcp/api/integration_tests.py
@@ -216,6 +216,62 @@ class IntegrationTests(unittest.TestCase):
       }]
   }
 
+  _VULN_RUSTSEC_2020_0105 = {
+      'id':
+          'RUSTSEC-2020-0105',
+      'summary':
+          'Update unsound DrainFilter and RString::retain',
+      'details':
+          'Affected versions of this crate contained code from the '
+          'Rust standard library that contained soundness bugs '
+          'rust-lang/rust#60977 (double drop) & rust-lang/rust#78498 '
+          '(create invalid utf-8 string).\n\n'
+          'The flaw was corrected in v0.9.1 by making a similar fix '
+          'to the one made in the Rust standard library.',
+      'aliases': ['CVE-2020-36212', 'CVE-2020-36213'],
+      'published':
+          '2020-12-21T12:00:00Z',
+      'references': [{
+          'type': 'PACKAGE',
+          'url': 'https://crates.io/crates/abi_stable'
+      }, {
+          'type': 'ADVISORY',
+          'url': 'https://rustsec.org/advisories/RUSTSEC-2020-0105.html'
+      }, {
+          'type': 'REPORT',
+          'url': 'https://github.com/rodrimati1992/abi_stable_crates/issues/44'
+      }],
+      'affected': [{
+          'package': {
+              'name': 'abi_stable',
+              'ecosystem': 'crates.io',
+              'purl': 'pkg:cargo/abi_stable'
+          },
+          'ranges': [{
+              'type': 'SEMVER',
+              'events': [{
+                  'introduced': '0.0.0-0'
+              }, {
+                  'fixed': '0.9.1'
+              }]
+          }],
+          'ecosystem_specific': {
+              'affects': {
+                  'functions': [],
+                  'arch': [],
+                  'os': []
+              }
+          },
+          'database_specific': {
+              'informational': None,
+              'cvss': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H',
+              'categories': ['memory-corruption'],
+              'source': 'https://github.com/rustsec/advisory-db/blob/'
+                        'osv/crates/RUSTSEC-2020-0105.json'
+          }
+      }]
+  }
+
   def setUp(self):
     self.maxDiff = None  # pylint: disable=invalid-name
 
@@ -370,6 +426,20 @@ def test_query_semver_multiple_package(self):
     self.assertCountEqual(['GO-2021-0061', 'GO-2020-0036'],
                           [vuln['id'] for vuln in response_json['vulns']])
 
+  def test_query_purl(self):
+    """Test querying by PURL."""
+    response = requests.post(
+        _api() + '/v1/query',
+        data=json.dumps({
+            'version': '0.9.0',
+            'package': {
+                'purl': 'pkg:cargo/abi_stable',
+            }
+        }))
+
+    self.assert_results_equal({'vulns': [self._VULN_RUSTSEC_2020_0105]},
+                              response.json())
+
 
 def print_logs(filename):
   """Print logs."""