Repository: google/osv.dev

Issue:
Make API queries work against generic "Debian" ecosystem
Currently, 

```
curl -X POST -d '{"version": "2.2.12-1+deb10u1",
    "package": {"name": "gnupg2", "ecosystem": "Debian:10"}}' "https://api.osv.dev/v1/query"
````

Works, but just specifying "Debian" doesn't, i.e.

```
curl -X POST -d '{"version": "2.2.12-1+deb10u1",      
    "package": {"name": "gnupg2", "ecosystem": "Debian"}}' "https://api.osv.dev/v1/query"
```

We should make the latter work as well, given that all Debian packages live in the same global version namespace. 



Discussion:
This is likely the offending line:

https://github.com/google/osv.dev/blob/3d7d9b67a0b226ed6d00e53cdb8aa78073afc7fa/gcp/api/server.py#L231


Resolving patch:
diff --git a/gcp/api/server.py b/gcp/api/server.py
--- a/gcp/api/server.py
+++ b/gcp/api/server.py
@@ -60,8 +60,7 @@ class OSVServicer(osv_service_v1_pb2_grpc.OSVServicer):
 
   @ndb_context
   def GetVulnById(self, request, context):
-    """Return a `Vulnerability` object for a given OSV ID.
-    """
+    """Return a `Vulnerability` object for a given OSV ID."""
     bug = osv.Bug.get_by_id(request.id)
     if not bug or bug.status == osv.BugStatus.UNPROCESSED:
       context.abort(grpc.StatusCode.NOT_FOUND, 'Bug not found.')
@@ -76,7 +75,8 @@ def GetVulnById(self, request, context):
   @ndb_context
   def QueryAffected(self, request, context):
     """Query vulnerabilities for a particular project at a given commit or
-    version."""
+    version.
+    """
     results = do_query(request.query, context).result()
     if results is not None:
       return osv_service_v1_pb2.VulnerabilityList(vulns=results)
@@ -186,7 +186,8 @@ def query_by_commit(commit, to_response=bug_to_response):
 def _is_semver_affected(affected_packages, package_name, ecosystem, purl,
                         version):
   """Returns whether or not the given version is within an affected SEMVER
-  range."""
+  range.
+  """
   version = semver_index.parse(version)
 
   affected = False
@@ -223,13 +224,18 @@ def _is_version_affected(affected_packages,
                          version,
                          normalize=False):
   """Returns whether or not the given version is within an affected ECOSYSTEM
-  range."""
+  range.
+  """
   for affected_package in affected_packages:
     if package_name and package_name != affected_package.package.name:
       continue
 
-    if ecosystem and ecosystem != affected_package.package.ecosystem:
-      continue
+    if ecosystem:
+      # If package ecosystem has a :, also try ignoring parts after it.
+      if (affected_package.package.ecosystem != ecosystem and
+          ecosystems.normalize(
+              affected_package.package.ecosystem) != ecosystem):
+        continue
 
     if purl and purl != affected_package.package.purl:
       continue
diff --git a/osv/ecosystems.py b/osv/ecosystems.py
--- a/osv/ecosystems.py
+++ b/osv/ecosystems.py
@@ -384,3 +384,7 @@ def set_cache(cache: Cache):
   """Configures and enables the redis caching layer"""
   global shared_cache
   shared_cache = cache
+
+
+def normalize(ecosystem_name: str):
+  return ecosystem_name.split(':')[0]
diff --git a/osv/models.py b/osv/models.py
--- a/osv/models.py
+++ b/osv/models.py
@@ -346,7 +346,7 @@ def _pre_put_hook(self):
 
     # For all ecosystems that specify a specific version with colon,
     # also add the base name
-    ecosystems_set.update({x.split(':')[0] for x in ecosystems_set})
+    ecosystems_set.update({ecosystems.normalize(x) for x in ecosystems_set})
 
     self.ecosystem = list(ecosystems_set)
     self.ecosystem.sort()


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
@@ -154,6 +154,56 @@ def test_query_version(self):
         }))
     self.assert_results_equal({'vulns': [self._VULN_744]}, response.json())
 
+  def test_query_debian(self):
+    """Test querying Debian with sub ecosystem versions"""
+    dsa_2665_1 = self._get('DSA-710-1')
+
+    response = requests.post(
+        _api() + '/v1/query',
+        data=json.dumps({
+            'version': '1.0.2-1',
+            'package': {
+                'name': 'gtkhtml',
+                'ecosystem': 'Debian',
+            }
+        }))
+    self.assert_results_equal({'vulns': [dsa_2665_1]}, response.json())
+
+    response = requests.post(
+        _api() + '/v1/query',
+        data=json.dumps({
+            'version': '1.0.2-1',
+            'package': {
+                'name': 'gtkhtml',
+                'ecosystem': 'Debian:3.0',
+            }
+        }))
+    self.assert_results_equal({'vulns': [dsa_2665_1]}, response.json())
+
+    # The vulnerbility does not exist in 4.0 release, so this should return
+    # with nothing
+    response = requests.post(
+        _api() + '/v1/query',
+        data=json.dumps({
+            'version': '1.0.2-1',
+            'package': {
+                'name': 'gtkhtml',
+                'ecosystem': 'Debian:4.0',
+            }
+        }))
+    self.assert_results_equal({}, response.json())
+
+    response = requests.post(
+        _api() + '/v1/query',
+        data=json.dumps({
+            'version': '1.0.2-1',
+            'package': {
+                'name': 'gtkhtml',
+                'ecosystem': 'Debian:9',
+            }
+        }))
+    self.assert_results_equal({}, response.json())
+
   def test_query_semver(self):
     """Test query by SemVer."""
     go_2020_0004 = self._get('GO-2020-0004')
diff --git a/gcp/api/test_server.py b/gcp/api/test_server.py
--- a/gcp/api/test_server.py
+++ b/gcp/api/test_server.py
@@ -86,7 +86,7 @@ def get_ip():
 def start_esp(port, backend_port, service_account_path, log_path):
   """Start ESPv2 frontend."""
   log_handle = open(log_path, 'w')
-  service_account_dir = os.path.dirname(service_account_path)
+  service_account_dir = os.path.dirname(os.path.abspath(service_account_path))
   service_account_name = os.path.basename(service_account_path)
 
   if os.getenv('CLOUDBUILD'):