text: # Copyright 2019 Google LLC
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #      http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  import json
  import logging
  import urllib.error
  import urllib.parse
  import urllib.request
  import ssl
  
  from flask import (
      Blueprint,
      redirect,
      flash,
      request,
      render_template,
      abort,
      url_for,
      Response,
      send_file,
      make_response,
      g,
  )
  from sqlalchemy import or_
  from sqlalchemy.orm import make_transient
  from werkzeug.routing import RequestRedirect
  from flask_bouncer import ensure
  from bouncer.constants import EDIT, CREATE
  
  from app import flash_error
  from app.auth.acls import admin_required, requires, skip_authorization, bouncer
  from app.exceptions import InvalidIdentifierException
  from app.vulnerability.views.details import VulnerabilityDetails
  import cfg
  from app.vulnerability.views.vulnerability import VulnerabilityView
  from data.models import RepositoryFilesSchema, Vulnerability
  from data.forms import VulnerabilityDeleteForm, VulnerabilityDetailsForm, VulnerabilityProposalReject, \
      VulnerabilityProposalApprove, VulnerabilityProposalAssign, VulnerabilityProposalPublish, \
      VulnerabilityProposalUnassign
  from data.database import DEFAULT_DATABASE
  from data.models.vulnerability import VulnerabilityState, ASSIGN, APPROVE, REJECT
  from lib.vcs_management import get_vcs_handler
  from lib.utils import create_json_response
  
  bp = Blueprint("vuln", __name__, url_prefix="/")
  db = DEFAULT_DATABASE
  
  
  def view_vuln(vcdb_id, use_template):
      try:
          vulnerability_details = VulnerabilityDetails(vcdb_id)
          vulnerability_details.validate_and_simplify_id()
          if not vulnerability_details.vulnerability_view:
              abort(404)
      except InvalidIdentifierException as err:
          return flash_error(str(err), "frontend.serve_index")
      return render_template(use_template,
                             vulnerability_details=vulnerability_details)
  
  
  @bp.route("/vuln", methods=["POST"])
  def vuln_view_post():
      return view_vuln(None, "vulnerability/view_overview.html")
  
  
  def _get_vulnerability_details(vcdb_id, vuln_id=None,
                                 simplify_id: bool = True):
      try:
          vulnerability_details = VulnerabilityDetails(vcdb_id, vuln_id)
          if simplify_id:
              vulnerability_details.validate_and_simplify_id()
          # Drop everything else.
          if not vulnerability_details.vulnerability_view:
              abort(404)
          return vulnerability_details
      except InvalidIdentifierException:
          abort(404)
  
  
  @bp.route("/<vcdb_id>/review/<vuln_id>", methods=["GET", "POST"])
  @skip_authorization  # authz is done inline
  def vuln_review(vcdb_id, vuln_id):
      vulnerability_details = _get_vulnerability_details(vcdb_id,
                                                         simplify_id=False)
      view = vulnerability_details.vulnerability_view
      vuln = vulnerability_details.get_or_create_vulnerability()
  
      proposal_vulnerability_details = _get_vulnerability_details(
          None, vuln_id=vuln_id, simplify_id=False)
      proposal_view = proposal_vulnerability_details.vulnerability_view
      proposal_vuln = proposal_vulnerability_details.get_or_create_vulnerability(
      )
  
      form_reject = VulnerabilityProposalReject()
      form_approve = VulnerabilityProposalApprove()
      form_assign = VulnerabilityProposalAssign()
      form_unassign = VulnerabilityProposalUnassign()
      form_publish = VulnerabilityProposalPublish()
  
      if request.method == 'POST':
          if request.form[
                  "review_response"] == "assign" and form_assign.validate_on_submit(
                  ):
              ensure(ASSIGN, proposal_vuln)
              if proposal_vuln.is_reviewable():
                  proposal_vuln.accept_review(g.user)
                  db.session.add(proposal_vuln)
                  db.session.commit()
                  flash("The review was successfully assigned to you.",
                        "success")
                  return redirect(request.url)
              else:
                  flash_error("This entry is not in a reviewable state.")
  
          if request.form[
              "review_response"] == "unassign" and form_unassign.validate_on_submit(
          ):
              ensure(ASSIGN, proposal_vuln)
              if proposal_vuln.is_reviewer(g.user):
                  proposal_vuln.deny_review()
                  db.session.add(proposal_vuln)
                  db.session.commit()
                  flash("You successfully unassigned yourself from this review.",
                        "success")
                  return redirect(request.url)
              else:
                  flash_error("This entry is not assigned to you.")
  
          if request.form[
                  "review_response"] == "approve" and form_approve.validate_on_submit(
                  ):
              ensure(APPROVE, proposal_vuln)
              proposal_vuln.accept_change()
              db.session.add(proposal_vuln)
              db.session.commit()
              flash(
                  "You approved the proposal. Waiting for the entry to be published by an admin.",
                  "success")
              return redirect(request.url)
  
          if request.form[
                  "review_response"] == "reject" and form_reject.validate_on_submit(
                  ):
              ensure(REJECT, proposal_vuln)
              proposal_vuln.deny_change(form_reject.data["review_feedback"])
              db.session.add(proposal_vuln)
              db.session.commit()
              flash("Waiting for the author to address your feedback.",
                    "success")
              return redirect(request.url)
  
          if request.form[
                  "review_response"] == "publish" and form_publish.validate_on_submit(
                  ):
              ensure('PUBLISH', proposal_vuln)
              proposal_vuln.publish_change()
              db.session.add(proposal_vuln)
              db.session.commit()
              # This might be the first entry of its kind so no archiving is necessary.
              if vuln.state:
                  vuln.archive_entry()
                  db.session.add(vuln)
                  db.session.commit()
              flash("Entry was successfully published.", "success")
              return redirect(request.url)
  
      # Published entries can't be reviewed.
      # if view.state == VulnerabilityState.PUBLISHED:
      #    raise RequestRedirect("/" + str(vcdb_id))
      return render_template(
          "vulnerability/review/review.html",
          proposal_vulnerability_details=proposal_vulnerability_details,
          vulnerability_details=vulnerability_details,
          form_assign=form_assign,
          form_unassign=form_unassign,
          form_reject=form_reject,
          form_approve=form_approve,
          form_publish=form_publish)
  
  
  # Create a catch all route for vulnerability identifiers.
  @bp.route("/<vcdb_id>")
  @skip_authorization
  def vuln_view(vcdb_id=None):
      vulnerability_details = _get_vulnerability_details(vcdb_id)
      view = vulnerability_details.vulnerability_view
      use_template = "vulnerability/view_details.html"
      if view.annotated:
          use_template = "vulnerability/view_overview.html"
      return render_template(use_template,
                             vulnerability_details=vulnerability_details)
  
  
  @bp.route("/<vcdb_id>/details")
  @skip_authorization
  def vuln_view_details(vcdb_id):
      return view_vuln(vcdb_id, "vulnerability/view_details.html")
  
  
  @bp.route("/<vcdb_id>/editor")
  @skip_authorization
  def vuln_editor(vcdb_id):
      return view_vuln(vcdb_id, "vulnerability/code_editor.html")
  
  
  @bp.route("/<vcdb_id>/tree")
  @skip_authorization
  def vuln_file_tree(vcdb_id):
      vulnerability_details = _get_vulnerability_details(vcdb_id)
      view = vulnerability_details.vulnerability_view
      master_commit = view.master_commit
  
      if not master_commit:
          abort(404)
  
      status_code = 200
      content_type = "text/json"
      response_msg = master_commit.tree_cache
      if not response_msg:
          try:
              vulnerability_details.fetch_tree_cache(skip_errors=False,
                                                     max_timeout=10)
              response_msg = master_commit.tree_cache
          except urllib.error.HTTPError as err:
              status_code = err.code
              response_msg = "".join([
                  "VCS proxy is unreachable (it might be down).",
                  "\r\nHTTPError\r\n",
                  err.read(),
              ])
              content_type = "text/plain"
          except urllib.error.URLError as err:
              status_code = 400
              response_msg = "".join([
                  "VCS proxy is unreachable (it might be down).",
                  "\r\nURLError\r\n",
                  str(err.reason),
              ])
              content_type = "text/plain"
          except Exception:  # pylint: disable=broad-except
              status_code = 400
              content_type = "text/plain"
              response_msg = "VCS proxy is unreachable (it might be down)."
  
      return Response(response=response_msg,
                      status=status_code,
                      content_type=content_type)
  
  
  @bp.route("/<vcdb_id>/annotation_data")
  @skip_authorization
  def annotation_data(vcdb_id):
      vulnerability_details = _get_vulnerability_details(vcdb_id)
      vulnerability_details.validate_and_simplify_id()
      view = vulnerability_details.vulnerability_view
      master_commit = view.master_commit
      if not master_commit:
          logging.error("Vuln (id: %r) has no linked Git commits!", view.id)
          return create_json_response("Entry has no linked Git link!", 404)
  
      master_commit = vulnerability_details.get_master_commit()
      files_schema = RepositoryFilesSchema(many=True)
      return files_schema.jsonify(master_commit.repository_files)
  
  
  @bp.route("/<vcdb_id>/file_provider")
  @skip_authorization
  def file_provider(vcdb_id):
      vulnerability_details = _get_vulnerability_details(vcdb_id)
      vulnerability_details.validate_and_simplify_id()
  
      item_hash = request.args.get("item_hash", 0, type=str)
      item_path = request.args.get("item_path", None, type=str)
  
      proxy_target = (cfg.GCE_VCS_PROXY_URL + url_for(
          "vcs_proxy.main_api",
          repo_url=vulnerability_details.repo_url,
          item_path=item_path,
          item_hash=item_hash,
      )[1:])
      try:
          ctx = ssl.create_default_context()
          ctx.check_hostname = False
          ctx.load_verify_locations(cafile=cfg.APP_CERT_FILE)
          ctx.verify_mode = ssl.CERT_REQUIRED
          result = urllib.request.urlopen(proxy_target, context=ctx)  # nosec
      except urllib.error.HTTPError as err:
          return Response(response=err.read(),
                          status=err.code,
                          content_type="text/plain")
      return send_file(result, mimetype="application/octet-stream")
  
  
  @bp.route("/<vcdb_id>/embed")
  @skip_authorization
  def embed(vcdb_id):
      try:
          section_id = int(request.args.get("sid", -1))
          start_line = int(request.args.get("start_line", 1))
          end_line = int(request.args.get("end_line", -1))
          vulnerability_details = VulnerabilityDetails(vcdb_id)
          vulnerability_details.validate_and_simplify_id()
          view = vulnerability_details.vulnerability_view
          if not view:
              return make_response(("No vulnerability found", 404))
          if not view.master_commit:
              return make_response(
                  (f"Vuln (id: {view.id}) has no linked Git commits!", 404))
  
          master_commit = vulnerability_details.get_master_commit()
          files_schema = RepositoryFilesSchema(many=True)
          # Hack to quickly retrieve the full data.
          custom_data = json.loads(
              files_schema.jsonify(master_commit.repository_files).data)
          settings = {
              "section_id": section_id,
              "startLine": start_line,
              "endLine": end_line,
              "entry_data": custom_data,
          }
          return render_template(
              "vulnerability/embedded.html",
              vulnerability_details=vulnerability_details,
              embed_settings=settings,
          )
      except (ValueError, InvalidIdentifierException):
          return make_response(("No vulnerability found", 404))
  
  
  @bp.route("/<vcdb_id>/create", methods=["GET", "POST"])
  @bp.route("/create", methods=["GET", "POST"])
  @requires(CREATE, Vulnerability)
  def create_vuln(vcdb_id=None):
      return _create_vuln_internal(vcdb_id)
  
  
  def _create_vuln_internal(vcdb_id=None):
      try:
          vulnerability_details = VulnerabilityDetails(vcdb_id)
          vulnerability = vulnerability_details.get_or_create_vulnerability()
      except InvalidIdentifierException as err:
          return flash_error(str(err), "frontend.serve_index")
  
      if vulnerability.id:
          logging.debug("Preexisting vulnerability entry found: %r",
                        vulnerability.id)
          delete_form = VulnerabilityDeleteForm()
          if delete_form.validate_on_submit():
              db.session.delete(vulnerability)
              # Remove the entry.
              db.session.commit()
              flash("The entry was deleted.", "success")
              return redirect("/")
  
      form = VulnerabilityDetailsForm(obj=vulnerability)
      commit = form.data["commits"][0]
      if not commit["repo_name"]:
          logging.info("Empty repository name. %r", commit)
          repo_url = commit["repo_url"]
          vcs_handler = get_vcs_handler(None, repo_url)
          if vcs_handler:
              logging.info("Found name. %r", vcs_handler.repo_name)
              form.commits[0].repo_name.process_data(vcs_handler.repo_name)
  
      if form.validate_on_submit():
          try:
              form.populate_obj(vulnerability)
              db.session.add(vulnerability)
              db.session.commit()
              # TODO: Improve this hack to assign a new vcdb_id here.
              #       Currently, we are just piggy backing on the auto increment of the primary key to ensure uniqueness.
              #       This will likely be prone to race conditions.
              vulnerability.vcdb_id = vulnerability.id
              db.session.add(vulnerability)
              db.session.commit()
  
              logging.debug("Successfully created/updated entry: %r",
                            vulnerability.id)
              flash("Successfully created/updated entry.", "success")
              return redirect(
                  url_for("vuln.vuln_view", vcdb_id=vulnerability.vcdb_id))
          except InvalidIdentifierException as err:
              flash_error(str(err))
  
      return render_template("vulnerability/create.html",
                             vulnerability_details=vulnerability_details,
                             form=form)
  
  
  def add_proposal(vuln: Vulnerability, form: VulnerabilityDetailsForm):
      vuln_clone = vuln.copy()
      form.populate_obj(vuln_clone)
  
      vuln_clone.version = None
      vuln_clone.prev_version = vuln.version
      vuln_clone.state = VulnerabilityState.READY
      vuln_clone.creator = g.user
      # Reset any previous feedback data.
      vuln_clone.reviewer_id = None
      vuln_clone.review_feedback = None
  
      db.session.add(vuln_clone)
      db.session.commit()
      if not vuln_clone.vcdb_id:
          # TODO: Improve this hack to assign a new vcdb_id here.
          #       Currently, we are just piggy backing on the auto increment of the primary key to ensure uniqueness.
          #       This will likely be prone to race conditions.
          vuln_clone.vcdb_id = vuln_clone.id
          db.session.add(vuln_clone)
          db.session.commit()
  
      flash(
          "Your proposal will be reviewed soon. You can monitor progress in your Proposals Section.",
          "success")
  
  
  @bp.route("/<vcdb_id>/edit", methods=["GET", "POST"])
  @bp.route("/edit", methods=["GET", "POST"])
  @requires(EDIT, Vulnerability)
  def edit_vuln(vcdb_id=None):
      return _edit_vuln_internal(vcdb_id)
  
  
  def _can_add_proposal(vuln):
      # Conditions for creating a proposal:
      """
      - No pending open proposals by the same user.
      - Proposals can only be made for currently PUBLISHED entries only.
      """
      # TODO: Simplify or move away the query below.
      existing_user_proposals = Vulnerability.query.filter(
          or_(Vulnerability.vcdb_id == vuln.vcdb_id, Vulnerability.cve_id == vuln.cve_id), Vulnerability.creator == g.user,
          Vulnerability.state != VulnerabilityState.PUBLISHED,
          Vulnerability.state != VulnerabilityState.ARCHIVED).first()
      if existing_user_proposals:
          flash_error(
              "You already have a pending proposal for this entry. Please go to your proposals section."
          )
          return False
      return True
  
  
  def _edit_vuln_internal(vcdb_id: str = None):
      vulnerability_details = _get_vulnerability_details(vcdb_id,
                                                         simplify_id=False)
      view = vulnerability_details.vulnerability_view
      vuln = vulnerability_details.get_or_create_vulnerability()
  
      if not _can_add_proposal(vuln):
          return redirect(
              url_for("vuln.vuln_view", vcdb_id=vcdb_id))
  
      form = VulnerabilityDetailsForm(obj=vuln)
  
      # Populate the form data from the vulnerability view if necessary.
      if form.comment.data == "":
          form.comment.data = view.comment
  
      form_submitted = form.validate_on_submit()
      if form_submitted:
          add_proposal(vuln, form)
          return redirect(
              url_for("vuln.vuln_view", vcdb_id=vcdb_id))
  
      return render_template("vulnerability/edit.html",
                             vulnerability_details=vulnerability_details,
                             form=form)
  