Repository: google/osv.dev

Issue:
move oss-fuzz git ranges to database_specific. 
if an oss-fuzz bisection resolves to a range of commits, we should move the range to database_specific instead. 

the affects.ranges fields should have single commit entries. 


Discussion:

Resolving patch:
diff --git a/docker/worker/oss_fuzz.py b/docker/worker/oss_fuzz.py
--- a/docker/worker/oss_fuzz.py
+++ b/docker/worker/oss_fuzz.py
@@ -194,7 +194,6 @@ def process_bisect_task(oss_fuzz_dir, bisect_type, source_id, message):
 
 def set_bug_attributes(bug, regress_result, fix_result):
   """Set bug attributes from bisection results."""
-  repo_url = regress_result.repo_url or fix_result.repo_url
   issue_id = fix_result.issue_id or regress_result.issue_id
   project = fix_result.project or regress_result.project
   ecosystem = fix_result.ecosystem or regress_result.ecosystem
@@ -224,14 +223,6 @@ def set_bug_attributes(bug, regress_result, fix_result):
   bug.regressed = regress_result.commit or ''
   bug.fixed = fix_result.commit or ''
 
-  bug.affected_ranges = [
-      osv.AffectedRange(
-          type='GIT',
-          repo_url=repo_url,
-          introduced=bug.regressed,
-          fixed=bug.fixed)
-  ]
-
 
 def process_impact_task(source_id, message):
   """Process an impact task."""
@@ -305,7 +296,8 @@ def process_impact_task(source_id, message):
     # Not fixed.
     fix_commit = ''
 
-  if len(result.regress_commits) == 1:
+  if (len(result.regress_commits) == 1 and
+      osv.UNKNOWN_COMMIT not in regress_result.commit):
     regress_commit = result.regress_commits[0]
   else:
     regress_commit = regress_result.commit
@@ -322,14 +314,25 @@ def process_impact_task(source_id, message):
   existing_bug.affected_fuzzy = osv.normalize_tags(affected_tags)
   existing_bug.status = osv.BugStatus.PROCESSED
 
+  # For the AffectedRange, use the first commit in the regress commit range, and
+  # the last commit in the fix commit range.
+  introduced = result.regress_commits[0] if result.regress_commits else ''
+  fixed = result.fix_commits[-1] if result.fix_commits else ''
   existing_bug.affected_ranges = [
       osv.AffectedRange(
-          type='GIT',
-          repo_url=repo_url,
-          introduced=existing_bug.regressed,
-          fixed=existing_bug.fixed)
+          type='GIT', repo_url=repo_url, introduced=introduced, fixed=fixed),
   ]
 
+  # Expose range data in `database_specific`.
+  database_specific = {}
+  if ':' in existing_bug.regressed:
+    database_specific['introduced_range'] = existing_bug.regressed
+  if ':' in existing_bug.fixed:
+    database_specific['fixed_range'] = existing_bug.fixed
+
+  if database_specific:
+    existing_bug.database_specific = database_specific
+
   # Don't display additional ranges for imprecise commits, as they can be
   # confusing.
   if ':' in existing_bug.fixed or ':' in existing_bug.regressed:
diff --git a/lib/osv/impact.py b/lib/osv/impact.py
--- a/lib/osv/impact.py
+++ b/lib/osv/impact.py
@@ -91,50 +91,51 @@ def __init__(self, detect_cherrypicks=True):
   def get_affected(self, repo, regress_commit_or_range, fix_commit_or_range):
     """"Get list of affected tags and commits for a bug given regressed and
     fixed commits."""
-    # If multiple, assume any commit in the regression range cause the
-    # regression.
     regress_commits = get_commit_range(repo, regress_commit_or_range)
     if len(regress_commits) > COMMIT_RANGE_LIMIT:
       raise ImpactError('Too many commits in regression range.')
 
-    # If multiple, assume all commits are necessary for fixing the regression.
+    # If multiple, assume the first commit in the regression range cause the
+    # regression.
+    if regress_commits:
+      regress_commit = regress_commits[0]
+    else:
+      regress_commit = None
+
     fix_commits = get_commit_range(repo, fix_commit_or_range)
     if len(fix_commits) > COMMIT_RANGE_LIMIT:
       logging.warning('Too many commits in fix range.')
       # Rather than bail out here and potentially leaving a Bug as "unfixed"
-      # indefinitely, we do the best we can here, by assuming the last
-      # COMMIT_RANGE_LIMIT commits fix the bug.
-      fix_commits = fix_commits[-COMMIT_RANGE_LIMIT:]
+      # indefinitely, we continue.
 
-    # Special case: unknown status for earlier revisions.
-    unknown_earlier_revisions = UNKNOWN_COMMIT in regress_commit_or_range
+    # If multiple, assume the last commit is necessary for fixing the
+    # regression.
+    if fix_commits:
+      fix_commit = fix_commits[-1]
+    else:
+      fix_commit = None
 
     tags_with_bug = set()
-    for commit in regress_commits:
-      tags_with_bug.update(self.get_tags_with_commits(repo, [commit]))
+    tags_with_bug.update(self.get_tags_with_commit(repo, regress_commit))
 
     if not regress_commits:
       # If no introduced commit provided, assume all commits prior to fix are
       # vulnerable.
       tags_with_bug.update(get_all_tags(repo))
 
-    tags_with_fix = self.get_tags_with_commits(repo, fix_commits)
+    tags_with_fix = self.get_tags_with_commit(repo, fix_commit)
     affected_commits, affected_ranges = self.get_affected_range(
-        repo, regress_commits, fix_commits)
+        repo, regress_commit, fix_commit)
 
     if len(regress_commits) > 1 or len(fix_commits) > 1:
       # Don't return ranges if input regressed and fixed commits are not single
       # commits.
       affected_ranges = []
 
-    if unknown_earlier_revisions:
-      # Include the unknown marker in resulting entities.
-      regress_commits.insert(0, UNKNOWN_COMMIT)
-
     return AffectedResult(tags_with_bug, tags_with_fix, affected_commits,
                           affected_ranges, regress_commits, fix_commits)
 
-  def get_affected_range(self, repo, regress_commits, fix_commits):
+  def get_affected_range(self, repo, regress_commit, fix_commit):
     """Get affected range."""
     range_collector = RangeCollector()
     commits = set()
@@ -153,33 +154,27 @@ def get_affected_range(self, repo, regress_commits, fix_commits):
       # Get the earliest equivalent commit in the regression range.
       equivalent_regress_commit = None
       if self.detect_cherrypicks:
-        for regress_commit in regress_commits:
-          logging.info('Finding equivalent regress commit to %s in %s',
-                       regress_commit, ref)
-          equivalent_regress_commit = self.get_equivalent_commit(
-              repo, ref, regress_commit)
-          if equivalent_regress_commit:
-            break
+        logging.info('Finding equivalent regress commit to %s in %s',
+                     regress_commit, ref)
+        equivalent_regress_commit = self.get_equivalent_commit(
+            repo, ref, regress_commit)
       else:
-        if regress_commits:
-          equivalent_regress_commit = regress_commits[0]
+        if regress_commit:
+          equivalent_regress_commit = regress_commit
 
       # If regress_commits is provided, then we should find an equivalent.
-      if not equivalent_regress_commit and regress_commits:
+      if not equivalent_regress_commit and regress_commit:
         continue
 
       # Get the latest equivalent commit in the fix range.
       equivalent_fix_commit = None
       if self.detect_cherrypicks:
-        for fix_commit in fix_commits:
-          logging.info('Finding equivalent fix commit to %s in %s', fix_commit,
-                       ref)
-          equivalent_commit = self.get_equivalent_commit(repo, ref, fix_commit)
-          if equivalent_commit:
-            equivalent_fix_commit = equivalent_commit
+        logging.info('Finding equivalent fix commit to %s in %s', fix_commit,
+                     ref)
+        equivalent_fix_commit = self.get_equivalent_commit(
+            repo, ref, fix_commit)
       else:
-        if fix_commits:
-          equivalent_fix_commit = fix_commits[-1]
+        equivalent_fix_commit = fix_commit
 
       range_collector.add(equivalent_regress_commit, equivalent_fix_commit)
 
@@ -246,12 +241,12 @@ def get_equivalent_commit(self, repo, to_search, target_commit):
     # TODO(ochang): Possibly look at commit message, author etc.
     return None
 
-  def get_tags_with_commits(self, repo, commits):
+  def get_tags_with_commit(self, repo, commit):
     """Get tags with a given commit."""
-    if not commits:
+    if not commit:
       return set()
 
-    logging.info('Getting tags which contain %s', ','.join(commits))
+    logging.info('Getting tags which contain %s', commit)
 
     tags = [
         ref for ref in repo.listall_references() if ref.startswith(TAG_PREFIX)
@@ -260,24 +255,16 @@ def get_tags_with_commits(self, repo, commits):
     if self.detect_cherrypicks:
       affected = set()
       for tag in tags:
-        if all(
-            self.get_equivalent_commit(repo, tag, commit)
-            for commit in commits):
+        if self.get_equivalent_commit(repo, tag, commit):
           affected.add(tag[len(TAG_PREFIX):])
     else:
-      affected = None
       # pygit2 does not provide any easy to way to compute this efficiently, so
       # we use git here.
-      for commit in commits:
-        current = subprocess.check_output(
-            ['git', '-C', repo.path, 'tag', '--contains',
-             commit]).decode().splitlines()
-        if affected is None:
-          affected = set(current)
-        else:
-          affected &= set(current)
-
-    return affected
+      affected = subprocess.check_output(
+          ['git', '-C', repo.path, 'tag', '--contains',
+           commit]).decode().splitlines()
+
+    return set(affected)
 
 
 def get_commit_range(repo, commit_or_range):


Test patch:
diff --git a/docker/worker/worker_test.py b/docker/worker/worker_test.py
--- a/docker/worker/worker_test.py
+++ b/docker/worker/worker_test.py
@@ -281,8 +281,7 @@ def test_range(self):
             ],
             'affected_fuzzy': ['0-1-1', '0-1-1', '1', '0-1-1', '0-2'],
             'affected_ranges': [{
-                'fixed': 'b1c95a196f22d06fcf80df8c6691cd113d8fefff:'
-                         '36f0bd9549298b44f9ff2496c9dd1326b3a9d0e2',
+                'fixed': '36f0bd9549298b44f9ff2496c9dd1326b3a9d0e2',
                 'introduced': 'eefe8ec3f1f90d0e684890e810f3f21e8500a4cd',
                 'repo_url': 'https://repo.com/repo',
                 'type': 'GIT'
@@ -313,7 +312,10 @@ def test_range(self):
             'ecosystem_specific': {
                 'severity': 'MEDIUM'
             },
-            'database_specific': None,
+            'database_specific': {
+                'fixed_range': 'b1c95a196f22d06fcf80df8c6691cd113d8fefff:'
+                               '36f0bd9549298b44f9ff2496c9dd1326b3a9d0e2'
+            },
             'semver_fixed_indexes': [],
             'source': 'oss-fuzz',
         },
@@ -324,6 +326,7 @@ def test_range(self):
       self.assertEqual('project', commit.project)
 
     self.assertCountEqual([
+        'b9b3fd4732695b83c3068b7b6a14bb372ec31f98',
         'ff8cc32ba60ad9cbb3b23f0a82aad96ebe9ff76b',
         'febfac1940086bc1f6d3dc33fda0a1d1ba336209',
         '4c155795426727ea05575bd5904321def23c03f4',
@@ -378,12 +381,15 @@ def test_fixed_range_too_long(self):
             ],
             'affected_fuzzy': ['0-1-1', '0-1-1', '1', '0-1-1', '0-2'],
             'affected_ranges': [{
-                'fixed': 'eefe8ec3f1f90d0e684890e810f3f21e8500a4cd:'
-                         'b587c21c36a84e16cfc6b39eb68578d43b5281ad',
+                'fixed': 'b587c21c36a84e16cfc6b39eb68578d43b5281ad',
                 'introduced': 'eefe8ec3f1f90d0e684890e810f3f21e8500a4cd',
                 'repo_url': 'https://repo.com/repo',
                 'type': 'GIT'
             }],
+            'database_specific': {
+                'fixed_range': 'eefe8ec3f1f90d0e684890e810f3f21e8500a4cd:'
+                               'b587c21c36a84e16cfc6b39eb68578d43b5281ad'
+            },
             'regressed': 'eefe8ec3f1f90d0e684890e810f3f21e8500a4cd',
             'fixed': ('eefe8ec3f1f90d0e684890e810f3f21e8500a4cd:'
                       'b587c21c36a84e16cfc6b39eb68578d43b5281ad'),
@@ -410,7 +416,6 @@ def test_fixed_range_too_long(self):
             'ecosystem_specific': {
                 'severity': 'MEDIUM'
             },
-            'database_specific': None,
             'semver_fixed_indexes': [],
             'source': 'oss-fuzz',
         },
@@ -421,6 +426,7 @@ def test_fixed_range_too_long(self):
       self.assertEqual('project', commit.project)
 
     self.assertCountEqual([
+        'b9b3fd4732695b83c3068b7b6a14bb372ec31f98',
         'ff8cc32ba60ad9cbb3b23f0a82aad96ebe9ff76b',
         'febfac1940086bc1f6d3dc33fda0a1d1ba336209',
         '4c155795426727ea05575bd5904321def23c03f4',
@@ -474,14 +480,10 @@ def test_zero_regression_range(self):
                 ['branch-v0.1.1', 'branch_1_cherrypick_regress', 'v0.1.1'],
             'affected_fuzzy': ['0-1-1', '1', '0-1-1'],
             'affected_ranges': [{
-                'fixed':
-                    '8d8242f545e9cec3e6d0d2e3f5bde8be1c659735',
-                'introduced':
-                    'unknown:eefe8ec3f1f90d0e684890e810f3f21e8500a4cd',
-                'repo_url':
-                    'https://repo.com/repo',
-                'type':
-                    'GIT'
+                'fixed': '8d8242f545e9cec3e6d0d2e3f5bde8be1c659735',
+                'introduced': 'eefe8ec3f1f90d0e684890e810f3f21e8500a4cd',
+                'repo_url': 'https://repo.com/repo',
+                'type': 'GIT'
             }],
             'fixed': '8d8242f545e9cec3e6d0d2e3f5bde8be1c659735',
             'regressed': 'unknown:eefe8ec3f1f90d0e684890e810f3f21e8500a4cd',
@@ -508,7 +510,10 @@ def test_zero_regression_range(self):
             'ecosystem_specific': {
                 'severity': 'MEDIUM'
             },
-            'database_specific': None,
+            'database_specific': {
+                'introduced_range': 'unknown:'
+                                    'eefe8ec3f1f90d0e684890e810f3f21e8500a4cd'
+            },
             'semver_fixed_indexes': [],
             'source': 'oss-fuzz',
         },