We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
V2 UI: Grouped versions on vulnerability page
Group by the first component leading up to the first period. e.g. "1.", "2."

![image](https://user-images.githubusercontent.com/759062/162373317-33e6f7bb-ffbe-4b82-a376-dc7deafb8587.png)

If no period, maybe just group this under something generic.



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `gcp/appengine/frontend_handlers.py`
Content:
```
1 # Copyright 2021 Google LLC
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 """Handlers for the OSV web frontend."""
15 
16 import os
17 import math
18 
19 from flask import abort
20 from flask import Blueprint
21 from flask import jsonify
22 from flask import make_response
23 from flask import render_template
24 from flask import request
25 
26 import cache
27 import osv
28 import rate_limiter
29 import source_mapper
30 import utils
31 
32 blueprint = Blueprint('frontend_handlers', __name__)
33 
34 _BACKEND_ROUTE = '/backend'
35 _PAGE_SIZE = 16
36 _PAGE_LOOKAHEAD = 4
37 _REQUESTS_PER_MIN = 30
38 
39 if utils.is_prod():
40   redis_host = os.environ.get('REDISHOST', 'localhost')
41   redis_port = int(os.environ.get('REDISPORT', 6379))
42   limiter = rate_limiter.RateLimiter(
43       redis_host, redis_port, requests_per_min=_REQUESTS_PER_MIN)
44 
45   @blueprint.before_request
46   def check_rate_limit():
47     ip_addr = request.headers.get('X-Appengine-User-Ip', 'unknown')
48     if not limiter.check_request(ip_addr):
49       abort(429)
50 
51 
52 @blueprint.before_request
53 def check_cors_preflight():
54   """Handle CORS preflight requests."""
55   if request.method != 'OPTIONS':
56     return None
57 
58   response = make_response()
59   response.headers.add('Access-Control-Allow-Origin', 'http://localhost:8080')
60   response.headers.add('Access-Control-Allow-Methods', '*')
61   response.headers.add('Access-Control-Allow-Headers', '*')
62   return response
63 
64 
65 @blueprint.after_request
66 def add_cors_headers(response):
67   """Add CORS headers."""
68   response.headers.add('Access-Control-Allow-Origin', 'http://localhost:8080')
69   return response
70 
71 
72 @blueprint.route('/')
73 def index():
74   """Main page."""
75   return render_template('index.html')
76 
77 
78 @blueprint.route('/v2/')
79 def index_v2():
80   return render_template(
81       'home.html', ecosystem_counts=osv_get_ecosystem_counts())
82 
83 
84 @blueprint.route('/v2/about')
85 def about():
86   return render_template('about.html')
87 
88 
89 @blueprint.route('/v2/list')
90 def list_vulnerabilities():
91   """Main page."""
92   query = request.args.get('q', '')
93   page = int(request.args.get('page', 1))
94   ecosystem = request.args.get('ecosystem')
95   results = osv_query(query, page, False, ecosystem)
96 
97   # Fetch ecosystems by default. As an optimization, skip when rendering page
98   # fragments.
99   ecosystem_counts = osv_get_ecosystem_counts(
100   ) if not request.headers.get('Turbo-Frame') else None
101 
102   return render_template(
103       'list.html',
104       page=page,
105       query=query,
106       selected_ecosystem=ecosystem,
107       ecosystem_counts=ecosystem_counts,
108       vulnerabilities=results['items'])
109 
110 
111 @blueprint.route('/v2/vulnerability/<vuln_id>')
112 def vulnerability(vuln_id):
113   """Vulnerability page."""
114   vuln = osv_get_by_id(vuln_id)
115   return render_template('vulnerability.html', vulnerability=vuln)
116 
117 
118 def bug_to_response(bug, detailed=True):
119   """Convert a Bug entity to a response object."""
120   response = osv.vulnerability_to_dict(bug.to_vulnerability())
121   response.update({
122       'isFixed': bug.is_fixed,
123       'invalid': bug.status == osv.BugStatus.INVALID
124   })
125 
126   if detailed:
127     add_links(response)
128     add_source_info(bug, response)
129   return response
130 
131 
132 def add_links(bug):
133   """Add VCS links where possible."""
134 
135   for entry in bug.get('affected', []):
136     for i, affected_range in enumerate(entry.get('ranges', [])):
137       affected_range['id'] = i
138       if affected_range['type'] != 'GIT':
139         continue
140 
141       repo_url = affected_range.get('repo')
142       if not repo_url:
143         continue
144 
145       for event in affected_range.get('events', []):
146         if event.get('introduced'):
147           event['introduced_link'] = _commit_to_link(repo_url,
148                                                      event['introduced'])
149           continue
150 
151         if event.get('fixed'):
152           event['fixed_link'] = _commit_to_link(repo_url, event['fixed'])
153           continue
154 
155         if event.get('limit'):
156           event['limit_link'] = _commit_to_link(repo_url, event['limit'])
157           continue
158 
159 
160 def add_source_info(bug, response):
161   """Add source information to `response`."""
162   if bug.source_of_truth == osv.SourceOfTruth.INTERNAL:
163     response['source'] = 'INTERNAL'
164     return
165 
166   source_repo = osv.get_source_repository(bug.source)
167   if not source_repo or not source_repo.link:
168     return
169 
170   source_path = osv.source_path(source_repo, bug)
171   response['source'] = source_repo.link + source_path
172   response['source_link'] = response['source']
173 
174 
175 def _commit_to_link(repo_url, commit):
176   """Convert commit to link."""
177   vcs = source_mapper.get_vcs_viewer_for_url(repo_url)
178   if not vcs:
179     return None
180 
181   if ':' not in commit:
182     return vcs.get_source_url_for_revision(commit)
183 
184   commit_parts = commit.split(':')
185   if len(commit_parts) != 2:
186     return None
187 
188   start, end = commit_parts
189   if start == 'unknown':
190     return None
191 
192   return vcs.get_source_url_for_revision_diff(start, end)
193 
194 
195 def osv_get_ecosystems():
196   """Get list of ecosystems."""
197   query = osv.Bug.query(projection=[osv.Bug.ecosystem], distinct=True)
198   return sorted([bug.ecosystem[0] for bug in query if bug.ecosystem])
199 
200 
201 @cache.instance.cached(
202     timeout=24 * 60 * 60, key_prefix='osv_get_ecosystem_counts')
203 def osv_get_ecosystem_counts():
204   """Get count of vulnerabilities per ecosystem."""
205   counts = {}
206   ecosystems = osv_get_ecosystems()
207   for ecosystem in ecosystems:
208     counts[ecosystem] = osv.Bug.query(osv.Bug.ecosystem == ecosystem).count()
209 
210   return counts
211 
212 
213 def osv_query(search_string, page, affected_only, ecosystem):
214   """Run an OSV query."""
215   query = osv.Bug.query(osv.Bug.status == osv.BugStatus.PROCESSED,
216                         osv.Bug.public == True)  # pylint: disable=singleton-comparison
217 
218   if search_string:
219     query = query.filter(osv.Bug.search_indices == search_string.lower())
220 
221   if affected_only:
222     query = query.filter(osv.Bug.has_affected == True)  # pylint: disable=singleton-comparison
223 
224   if ecosystem:
225     query = query.filter(osv.Bug.ecosystem == ecosystem)
226 
227   query = query.order(-osv.Bug.last_modified)
228   total = query.count()
229   results = {
230       'total': total,
231       'items': [],
232   }
233 
234   bugs, _, _ = query.fetch_page(
235       page_size=_PAGE_SIZE, offset=(page - 1) * _PAGE_SIZE)
236   for bug in bugs:
237     results['items'].append(bug_to_response(bug, detailed=False))
238 
239   return results
240 
241 
242 def osv_get_by_id(vuln_id):
243   """Gets bug details from its id. If invalid, aborts the request."""
244   if not vuln_id:
245     abort(400)
246     return None
247 
248   bug = osv.Bug.get_by_id(vuln_id)
249   if not bug:
250     abort(404)
251     return None
252 
253   if bug.status == osv.BugStatus.UNPROCESSED:
254     abort(404)
255     return None
256 
257   if not bug.public:
258     abort(403)
259     return None
260 
261   return bug_to_response(bug)
262 
263 
264 @blueprint.route(_BACKEND_ROUTE + '/ecosystems')
265 def ecosystems_handler():
266   """Handle query for list of ecosystems."""
267   return jsonify(osv_get_ecosystems())
268 
269 
270 @blueprint.route(_BACKEND_ROUTE + '/ecosystem-counts')
271 def ecosystem_counts_handler():
272   """Handle query for list of ecosystems."""
273   return jsonify(osv_get_ecosystem_counts())
274 
275 
276 @blueprint.route(_BACKEND_ROUTE + '/query')
277 def query_handler():
278   """Handle a query."""
279   search_string = request.args.get('search')
280   page = int(request.args.get('page', 1))
281   affected_only = request.args.get('affected_only') == 'true'
282   ecosystem = request.args.get('ecosystem')
283   results = osv_query(search_string, page, affected_only, ecosystem)
284   return jsonify(results)
285 
286 
287 @blueprint.route(_BACKEND_ROUTE + '/vulnerability')
288 def vulnerability_handler():
289   """Handle a vulnerability request."""
290   vuln_id = request.args.get('id')
291   return jsonify(osv_get_by_id(vuln_id))
292 
293 
294 @blueprint.app_template_filter('event_type')
295 def event_type(event):
296   """Get the type from an event."""
297   if event.get('introduced'):
298     return 'Introduced'
299   if event.get('fixed'):
300     return 'Fixed'
301   if event.get('limit'):
302     return 'Limit'
303   if event.get('last_affected'):
304     return 'Last affected'
305 
306   return None
307 
308 
309 @blueprint.app_template_filter('event_link')
310 def event_link(event):
311   """Get the link from an event."""
312   if event.get('introduced_link'):
313     return event['introduced_link']
314   if event.get('fixed_link'):
315     return event['fixed_link']
316   if event.get('limit_link'):
317     return event['limit_link']
318   if event.get('last_affected_link'):
319     return event['last_affected_link']
320 
321   return None
322 
323 
324 @blueprint.app_template_filter('event_value')
325 def event_value(event):
326   """Get the value from an event."""
327   if event.get('introduced'):
328     return event['introduced']
329   if event.get('fixed'):
330     return event['fixed']
331   if event.get('limit'):
332     return event['limit']
333   if event.get('last_affected'):
334     return event['last_affected']
335 
336   return None
337 
338 
339 @blueprint.app_template_filter('should_collapse')
340 def should_collapse(affected):
341   """Whether if we should collapse the package tab bar."""
342   total_package_length = sum(
343       [len(entry.get('package', {}).get('name', '')) for entry in affected])
344   return total_package_length > 70 or len(affected) > 5
345 
346 
347 @blueprint.app_template_filter('log')
348 def logarithm(n):
349   return math.log(n)
350
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/gcp/appengine/frontend_handlers.py b/gcp/appengine/frontend_handlers.py
--- a/gcp/appengine/frontend_handlers.py
+++ b/gcp/appengine/frontend_handlers.py
@@ -344,6 +344,22 @@
   return total_package_length > 70 or len(affected) > 5
 
 
+@blueprint.app_template_filter('group_versions')
+def group_versions(versions):
+  """Group versions by prefix."""
+  groups = {}
+
+  for version in sorted(versions):
+    if '.' not in version:
+      groups.setdefault('Other', []).append(version)
+      continue
+
+    label = version.split('.')[0] + '.*'
+    groups.setdefault(label, []).append(version)
+
+  return groups
+
+
 @blueprint.app_template_filter('log')
 def logarithm(n):
   return math.log(n)

{"golden_diff": "diff --git a/gcp/appengine/frontend_handlers.py b/gcp/appengine/frontend_handlers.py\n--- a/gcp/appengine/frontend_handlers.py\n+++ b/gcp/appengine/frontend_handlers.py\n@@ -344,6 +344,22 @@\n   return total_package_length > 70 or len(affected) > 5\n \n \n+@blueprint.app_template_filter('group_versions')\n+def group_versions(versions):\n+  \"\"\"Group versions by prefix.\"\"\"\n+  groups = {}\n+\n+  for version in sorted(versions):\n+    if '.' not in version:\n+      groups.setdefault('Other', []).append(version)\n+      continue\n+\n+    label = version.split('.')[0] + '.*'\n+    groups.setdefault(label, []).append(version)\n+\n+  return groups\n+\n+\n @blueprint.app_template_filter('log')\n def logarithm(n):\n   return math.log(n)\n", "issue": "V2 UI: Grouped versions on vulnerability page\nGroup by the first component leading up to the first period. e.g. \"1.\", \"2.\"\r\n\r\n![image](https://user-images.githubusercontent.com/759062/162373317-33e6f7bb-ffbe-4b82-a376-dc7deafb8587.png)\r\n\r\nIf no period, maybe just group this under something generic.\r\n\r\n\n", "before_files": [{"content": "# Copyright 2021 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\"\"\"Handlers for the OSV web frontend.\"\"\"\n\nimport os\nimport math\n\nfrom flask import abort\nfrom flask import Blueprint\nfrom flask import jsonify\nfrom flask import make_response\nfrom flask import render_template\nfrom flask import request\n\nimport cache\nimport osv\nimport rate_limiter\nimport source_mapper\nimport utils\n\nblueprint = Blueprint('frontend_handlers', __name__)\n\n_BACKEND_ROUTE = '/backend'\n_PAGE_SIZE = 16\n_PAGE_LOOKAHEAD = 4\n_REQUESTS_PER_MIN = 30\n\nif utils.is_prod():\n  redis_host = os.environ.get('REDISHOST', 'localhost')\n  redis_port = int(os.environ.get('REDISPORT', 6379))\n  limiter = rate_limiter.RateLimiter(\n      redis_host, redis_port, requests_per_min=_REQUESTS_PER_MIN)\n\n  @blueprint.before_request\n  def check_rate_limit():\n    ip_addr = request.headers.get('X-Appengine-User-Ip', 'unknown')\n    if not limiter.check_request(ip_addr):\n      abort(429)\n\n\n@blueprint.before_request\ndef check_cors_preflight():\n  \"\"\"Handle CORS preflight requests.\"\"\"\n  if request.method != 'OPTIONS':\n    return None\n\n  response = make_response()\n  response.headers.add('Access-Control-Allow-Origin', 'http://localhost:8080')\n  response.headers.add('Access-Control-Allow-Methods', '*')\n  response.headers.add('Access-Control-Allow-Headers', '*')\n  return response\n\n\n@blueprint.after_request\ndef add_cors_headers(response):\n  \"\"\"Add CORS headers.\"\"\"\n  response.headers.add('Access-Control-Allow-Origin', 'http://localhost:8080')\n  return response\n\n\n@blueprint.route('/')\ndef index():\n  \"\"\"Main page.\"\"\"\n  return render_template('index.html')\n\n\n@blueprint.route('/v2/')\ndef index_v2():\n  return render_template(\n      'home.html', ecosystem_counts=osv_get_ecosystem_counts())\n\n\n@blueprint.route('/v2/about')\ndef about():\n  return render_template('about.html')\n\n\n@blueprint.route('/v2/list')\ndef list_vulnerabilities():\n  \"\"\"Main page.\"\"\"\n  query = request.args.get('q', '')\n  page = int(request.args.get('page', 1))\n  ecosystem = request.args.get('ecosystem')\n  results = osv_query(query, page, False, ecosystem)\n\n  # Fetch ecosystems by default. As an optimization, skip when rendering page\n  # fragments.\n  ecosystem_counts = osv_get_ecosystem_counts(\n  ) if not request.headers.get('Turbo-Frame') else None\n\n  return render_template(\n      'list.html',\n      page=page,\n      query=query,\n      selected_ecosystem=ecosystem,\n      ecosystem_counts=ecosystem_counts,\n      vulnerabilities=results['items'])\n\n\n@blueprint.route('/v2/vulnerability/<vuln_id>')\ndef vulnerability(vuln_id):\n  \"\"\"Vulnerability page.\"\"\"\n  vuln = osv_get_by_id(vuln_id)\n  return render_template('vulnerability.html', vulnerability=vuln)\n\n\ndef bug_to_response(bug, detailed=True):\n  \"\"\"Convert a Bug entity to a response object.\"\"\"\n  response = osv.vulnerability_to_dict(bug.to_vulnerability())\n  response.update({\n      'isFixed': bug.is_fixed,\n      'invalid': bug.status == osv.BugStatus.INVALID\n  })\n\n  if detailed:\n    add_links(response)\n    add_source_info(bug, response)\n  return response\n\n\ndef add_links(bug):\n  \"\"\"Add VCS links where possible.\"\"\"\n\n  for entry in bug.get('affected', []):\n    for i, affected_range in enumerate(entry.get('ranges', [])):\n      affected_range['id'] = i\n      if affected_range['type'] != 'GIT':\n        continue\n\n      repo_url = affected_range.get('repo')\n      if not repo_url:\n        continue\n\n      for event in affected_range.get('events', []):\n        if event.get('introduced'):\n          event['introduced_link'] = _commit_to_link(repo_url,\n                                                     event['introduced'])\n          continue\n\n        if event.get('fixed'):\n          event['fixed_link'] = _commit_to_link(repo_url, event['fixed'])\n          continue\n\n        if event.get('limit'):\n          event['limit_link'] = _commit_to_link(repo_url, event['limit'])\n          continue\n\n\ndef add_source_info(bug, response):\n  \"\"\"Add source information to `response`.\"\"\"\n  if bug.source_of_truth == osv.SourceOfTruth.INTERNAL:\n    response['source'] = 'INTERNAL'\n    return\n\n  source_repo = osv.get_source_repository(bug.source)\n  if not source_repo or not source_repo.link:\n    return\n\n  source_path = osv.source_path(source_repo, bug)\n  response['source'] = source_repo.link + source_path\n  response['source_link'] = response['source']\n\n\ndef _commit_to_link(repo_url, commit):\n  \"\"\"Convert commit to link.\"\"\"\n  vcs = source_mapper.get_vcs_viewer_for_url(repo_url)\n  if not vcs:\n    return None\n\n  if ':' not in commit:\n    return vcs.get_source_url_for_revision(commit)\n\n  commit_parts = commit.split(':')\n  if len(commit_parts) != 2:\n    return None\n\n  start, end = commit_parts\n  if start == 'unknown':\n    return None\n\n  return vcs.get_source_url_for_revision_diff(start, end)\n\n\ndef osv_get_ecosystems():\n  \"\"\"Get list of ecosystems.\"\"\"\n  query = osv.Bug.query(projection=[osv.Bug.ecosystem], distinct=True)\n  return sorted([bug.ecosystem[0] for bug in query if bug.ecosystem])\n\n\n@cache.instance.cached(\n    timeout=24 * 60 * 60, key_prefix='osv_get_ecosystem_counts')\ndef osv_get_ecosystem_counts():\n  \"\"\"Get count of vulnerabilities per ecosystem.\"\"\"\n  counts = {}\n  ecosystems = osv_get_ecosystems()\n  for ecosystem in ecosystems:\n    counts[ecosystem] = osv.Bug.query(osv.Bug.ecosystem == ecosystem).count()\n\n  return counts\n\n\ndef osv_query(search_string, page, affected_only, ecosystem):\n  \"\"\"Run an OSV query.\"\"\"\n  query = osv.Bug.query(osv.Bug.status == osv.BugStatus.PROCESSED,\n                        osv.Bug.public == True)  # pylint: disable=singleton-comparison\n\n  if search_string:\n    query = query.filter(osv.Bug.search_indices == search_string.lower())\n\n  if affected_only:\n    query = query.filter(osv.Bug.has_affected == True)  # pylint: disable=singleton-comparison\n\n  if ecosystem:\n    query = query.filter(osv.Bug.ecosystem == ecosystem)\n\n  query = query.order(-osv.Bug.last_modified)\n  total = query.count()\n  results = {\n      'total': total,\n      'items': [],\n  }\n\n  bugs, _, _ = query.fetch_page(\n      page_size=_PAGE_SIZE, offset=(page - 1) * _PAGE_SIZE)\n  for bug in bugs:\n    results['items'].append(bug_to_response(bug, detailed=False))\n\n  return results\n\n\ndef osv_get_by_id(vuln_id):\n  \"\"\"Gets bug details from its id. If invalid, aborts the request.\"\"\"\n  if not vuln_id:\n    abort(400)\n    return None\n\n  bug = osv.Bug.get_by_id(vuln_id)\n  if not bug:\n    abort(404)\n    return None\n\n  if bug.status == osv.BugStatus.UNPROCESSED:\n    abort(404)\n    return None\n\n  if not bug.public:\n    abort(403)\n    return None\n\n  return bug_to_response(bug)\n\n\n@blueprint.route(_BACKEND_ROUTE + '/ecosystems')\ndef ecosystems_handler():\n  \"\"\"Handle query for list of ecosystems.\"\"\"\n  return jsonify(osv_get_ecosystems())\n\n\n@blueprint.route(_BACKEND_ROUTE + '/ecosystem-counts')\ndef ecosystem_counts_handler():\n  \"\"\"Handle query for list of ecosystems.\"\"\"\n  return jsonify(osv_get_ecosystem_counts())\n\n\n@blueprint.route(_BACKEND_ROUTE + '/query')\ndef query_handler():\n  \"\"\"Handle a query.\"\"\"\n  search_string = request.args.get('search')\n  page = int(request.args.get('page', 1))\n  affected_only = request.args.get('affected_only') == 'true'\n  ecosystem = request.args.get('ecosystem')\n  results = osv_query(search_string, page, affected_only, ecosystem)\n  return jsonify(results)\n\n\n@blueprint.route(_BACKEND_ROUTE + '/vulnerability')\ndef vulnerability_handler():\n  \"\"\"Handle a vulnerability request.\"\"\"\n  vuln_id = request.args.get('id')\n  return jsonify(osv_get_by_id(vuln_id))\n\n\n@blueprint.app_template_filter('event_type')\ndef event_type(event):\n  \"\"\"Get the type from an event.\"\"\"\n  if event.get('introduced'):\n    return 'Introduced'\n  if event.get('fixed'):\n    return 'Fixed'\n  if event.get('limit'):\n    return 'Limit'\n  if event.get('last_affected'):\n    return 'Last affected'\n\n  return None\n\n\n@blueprint.app_template_filter('event_link')\ndef event_link(event):\n  \"\"\"Get the link from an event.\"\"\"\n  if event.get('introduced_link'):\n    return event['introduced_link']\n  if event.get('fixed_link'):\n    return event['fixed_link']\n  if event.get('limit_link'):\n    return event['limit_link']\n  if event.get('last_affected_link'):\n    return event['last_affected_link']\n\n  return None\n\n\n@blueprint.app_template_filter('event_value')\ndef event_value(event):\n  \"\"\"Get the value from an event.\"\"\"\n  if event.get('introduced'):\n    return event['introduced']\n  if event.get('fixed'):\n    return event['fixed']\n  if event.get('limit'):\n    return event['limit']\n  if event.get('last_affected'):\n    return event['last_affected']\n\n  return None\n\n\n@blueprint.app_template_filter('should_collapse')\ndef should_collapse(affected):\n  \"\"\"Whether if we should collapse the package tab bar.\"\"\"\n  total_package_length = sum(\n      [len(entry.get('package', {}).get('name', '')) for entry in affected])\n  return total_package_length > 70 or len(affected) > 5\n\n\n@blueprint.app_template_filter('log')\ndef logarithm(n):\n  return math.log(n)\n", "path": "gcp/appengine/frontend_handlers.py"}], "after_files": [{"content": "# Copyright 2021 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\"\"\"Handlers for the OSV web frontend.\"\"\"\n\nimport os\nimport math\n\nfrom flask import abort\nfrom flask import Blueprint\nfrom flask import jsonify\nfrom flask import make_response\nfrom flask import render_template\nfrom flask import request\n\nimport cache\nimport osv\nimport rate_limiter\nimport source_mapper\nimport utils\n\nblueprint = Blueprint('frontend_handlers', __name__)\n\n_BACKEND_ROUTE = '/backend'\n_PAGE_SIZE = 16\n_PAGE_LOOKAHEAD = 4\n_REQUESTS_PER_MIN = 30\n\nif utils.is_prod():\n  redis_host = os.environ.get('REDISHOST', 'localhost')\n  redis_port = int(os.environ.get('REDISPORT', 6379))\n  limiter = rate_limiter.RateLimiter(\n      redis_host, redis_port, requests_per_min=_REQUESTS_PER_MIN)\n\n  @blueprint.before_request\n  def check_rate_limit():\n    ip_addr = request.headers.get('X-Appengine-User-Ip', 'unknown')\n    if not limiter.check_request(ip_addr):\n      abort(429)\n\n\n@blueprint.before_request\ndef check_cors_preflight():\n  \"\"\"Handle CORS preflight requests.\"\"\"\n  if request.method != 'OPTIONS':\n    return None\n\n  response = make_response()\n  response.headers.add('Access-Control-Allow-Origin', 'http://localhost:8080')\n  response.headers.add('Access-Control-Allow-Methods', '*')\n  response.headers.add('Access-Control-Allow-Headers', '*')\n  return response\n\n\n@blueprint.after_request\ndef add_cors_headers(response):\n  \"\"\"Add CORS headers.\"\"\"\n  response.headers.add('Access-Control-Allow-Origin', 'http://localhost:8080')\n  return response\n\n\n@blueprint.route('/')\ndef index():\n  \"\"\"Main page.\"\"\"\n  return render_template('index.html')\n\n\n@blueprint.route('/v2/')\ndef index_v2():\n  return render_template(\n      'home.html', ecosystem_counts=osv_get_ecosystem_counts())\n\n\n@blueprint.route('/v2/about')\ndef about():\n  return render_template('about.html')\n\n\n@blueprint.route('/v2/list')\ndef list_vulnerabilities():\n  \"\"\"Main page.\"\"\"\n  query = request.args.get('q', '')\n  page = int(request.args.get('page', 1))\n  ecosystem = request.args.get('ecosystem')\n  results = osv_query(query, page, False, ecosystem)\n\n  # Fetch ecosystems by default. As an optimization, skip when rendering page\n  # fragments.\n  ecosystem_counts = osv_get_ecosystem_counts(\n  ) if not request.headers.get('Turbo-Frame') else None\n\n  return render_template(\n      'list.html',\n      page=page,\n      query=query,\n      selected_ecosystem=ecosystem,\n      ecosystem_counts=ecosystem_counts,\n      vulnerabilities=results['items'])\n\n\n@blueprint.route('/v2/vulnerability/<vuln_id>')\ndef vulnerability(vuln_id):\n  \"\"\"Vulnerability page.\"\"\"\n  vuln = osv_get_by_id(vuln_id)\n  return render_template('vulnerability.html', vulnerability=vuln)\n\n\ndef bug_to_response(bug, detailed=True):\n  \"\"\"Convert a Bug entity to a response object.\"\"\"\n  response = osv.vulnerability_to_dict(bug.to_vulnerability())\n  response.update({\n      'isFixed': bug.is_fixed,\n      'invalid': bug.status == osv.BugStatus.INVALID\n  })\n\n  if detailed:\n    add_links(response)\n    add_source_info(bug, response)\n  return response\n\n\ndef add_links(bug):\n  \"\"\"Add VCS links where possible.\"\"\"\n\n  for entry in bug.get('affected', []):\n    for i, affected_range in enumerate(entry.get('ranges', [])):\n      affected_range['id'] = i\n      if affected_range['type'] != 'GIT':\n        continue\n\n      repo_url = affected_range.get('repo')\n      if not repo_url:\n        continue\n\n      for event in affected_range.get('events', []):\n        if event.get('introduced'):\n          event['introduced_link'] = _commit_to_link(repo_url,\n                                                     event['introduced'])\n          continue\n\n        if event.get('fixed'):\n          event['fixed_link'] = _commit_to_link(repo_url, event['fixed'])\n          continue\n\n        if event.get('limit'):\n          event['limit_link'] = _commit_to_link(repo_url, event['limit'])\n          continue\n\n\ndef add_source_info(bug, response):\n  \"\"\"Add source information to `response`.\"\"\"\n  if bug.source_of_truth == osv.SourceOfTruth.INTERNAL:\n    response['source'] = 'INTERNAL'\n    return\n\n  source_repo = osv.get_source_repository(bug.source)\n  if not source_repo or not source_repo.link:\n    return\n\n  source_path = osv.source_path(source_repo, bug)\n  response['source'] = source_repo.link + source_path\n  response['source_link'] = response['source']\n\n\ndef _commit_to_link(repo_url, commit):\n  \"\"\"Convert commit to link.\"\"\"\n  vcs = source_mapper.get_vcs_viewer_for_url(repo_url)\n  if not vcs:\n    return None\n\n  if ':' not in commit:\n    return vcs.get_source_url_for_revision(commit)\n\n  commit_parts = commit.split(':')\n  if len(commit_parts) != 2:\n    return None\n\n  start, end = commit_parts\n  if start == 'unknown':\n    return None\n\n  return vcs.get_source_url_for_revision_diff(start, end)\n\n\ndef osv_get_ecosystems():\n  \"\"\"Get list of ecosystems.\"\"\"\n  query = osv.Bug.query(projection=[osv.Bug.ecosystem], distinct=True)\n  return sorted([bug.ecosystem[0] for bug in query if bug.ecosystem])\n\n\n@cache.instance.cached(\n    timeout=24 * 60 * 60, key_prefix='osv_get_ecosystem_counts')\ndef osv_get_ecosystem_counts():\n  \"\"\"Get count of vulnerabilities per ecosystem.\"\"\"\n  counts = {}\n  ecosystems = osv_get_ecosystems()\n  for ecosystem in ecosystems:\n    counts[ecosystem] = osv.Bug.query(osv.Bug.ecosystem == ecosystem).count()\n\n  return counts\n\n\ndef osv_query(search_string, page, affected_only, ecosystem):\n  \"\"\"Run an OSV query.\"\"\"\n  query = osv.Bug.query(osv.Bug.status == osv.BugStatus.PROCESSED,\n                        osv.Bug.public == True)  # pylint: disable=singleton-comparison\n\n  if search_string:\n    query = query.filter(osv.Bug.search_indices == search_string.lower())\n\n  if affected_only:\n    query = query.filter(osv.Bug.has_affected == True)  # pylint: disable=singleton-comparison\n\n  if ecosystem:\n    query = query.filter(osv.Bug.ecosystem == ecosystem)\n\n  query = query.order(-osv.Bug.last_modified)\n  total = query.count()\n  results = {\n      'total': total,\n      'items': [],\n  }\n\n  bugs, _, _ = query.fetch_page(\n      page_size=_PAGE_SIZE, offset=(page - 1) * _PAGE_SIZE)\n  for bug in bugs:\n    results['items'].append(bug_to_response(bug, detailed=False))\n\n  return results\n\n\ndef osv_get_by_id(vuln_id):\n  \"\"\"Gets bug details from its id. If invalid, aborts the request.\"\"\"\n  if not vuln_id:\n    abort(400)\n    return None\n\n  bug = osv.Bug.get_by_id(vuln_id)\n  if not bug:\n    abort(404)\n    return None\n\n  if bug.status == osv.BugStatus.UNPROCESSED:\n    abort(404)\n    return None\n\n  if not bug.public:\n    abort(403)\n    return None\n\n  return bug_to_response(bug)\n\n\n@blueprint.route(_BACKEND_ROUTE + '/ecosystems')\ndef ecosystems_handler():\n  \"\"\"Handle query for list of ecosystems.\"\"\"\n  return jsonify(osv_get_ecosystems())\n\n\n@blueprint.route(_BACKEND_ROUTE + '/ecosystem-counts')\ndef ecosystem_counts_handler():\n  \"\"\"Handle query for list of ecosystems.\"\"\"\n  return jsonify(osv_get_ecosystem_counts())\n\n\n@blueprint.route(_BACKEND_ROUTE + '/query')\ndef query_handler():\n  \"\"\"Handle a query.\"\"\"\n  search_string = request.args.get('search')\n  page = int(request.args.get('page', 1))\n  affected_only = request.args.get('affected_only') == 'true'\n  ecosystem = request.args.get('ecosystem')\n  results = osv_query(search_string, page, affected_only, ecosystem)\n  return jsonify(results)\n\n\n@blueprint.route(_BACKEND_ROUTE + '/vulnerability')\ndef vulnerability_handler():\n  \"\"\"Handle a vulnerability request.\"\"\"\n  vuln_id = request.args.get('id')\n  return jsonify(osv_get_by_id(vuln_id))\n\n\n@blueprint.app_template_filter('event_type')\ndef event_type(event):\n  \"\"\"Get the type from an event.\"\"\"\n  if event.get('introduced'):\n    return 'Introduced'\n  if event.get('fixed'):\n    return 'Fixed'\n  if event.get('limit'):\n    return 'Limit'\n  if event.get('last_affected'):\n    return 'Last affected'\n\n  return None\n\n\n@blueprint.app_template_filter('event_link')\ndef event_link(event):\n  \"\"\"Get the link from an event.\"\"\"\n  if event.get('introduced_link'):\n    return event['introduced_link']\n  if event.get('fixed_link'):\n    return event['fixed_link']\n  if event.get('limit_link'):\n    return event['limit_link']\n  if event.get('last_affected_link'):\n    return event['last_affected_link']\n\n  return None\n\n\n@blueprint.app_template_filter('event_value')\ndef event_value(event):\n  \"\"\"Get the value from an event.\"\"\"\n  if event.get('introduced'):\n    return event['introduced']\n  if event.get('fixed'):\n    return event['fixed']\n  if event.get('limit'):\n    return event['limit']\n  if event.get('last_affected'):\n    return event['last_affected']\n\n  return None\n\n\n@blueprint.app_template_filter('should_collapse')\ndef should_collapse(affected):\n  \"\"\"Whether if we should collapse the package tab bar.\"\"\"\n  total_package_length = sum(\n      [len(entry.get('package', {}).get('name', '')) for entry in affected])\n  return total_package_length > 70 or len(affected) > 5\n\n\n@blueprint.app_template_filter('group_versions')\ndef group_versions(versions):\n  \"\"\"Group versions by prefix.\"\"\"\n  groups = {}\n\n  for version in sorted(versions):\n    if '.' not in version:\n      groups.setdefault('Other', []).append(version)\n      continue\n\n    label = version.split('.')[0] + '.*'\n    groups.setdefault(label, []).append(version)\n\n  return groups\n\n\n@blueprint.app_template_filter('log')\ndef logarithm(n):\n  return math.log(n)\n", "path": "gcp/appengine/frontend_handlers.py"}]}