def escape_markdown_characters(text)

  ["\\","#"].each do |char|
    text = text.gsub(char,"\\"+char)
  end
  return text
end

class Vulnerability
  attr_accessor :id, :name, :external_link,
    :external_id, :type, :source, :severity,
    :identified, :status, :attack_vectors,
    :url, :confidence_level, :source_code_file,
    :source_code_line, :source_code, :details, :term,
    :score, :file_path, :file_name, :task_name, :task_id,
    :key_suffix, :code_fragment, :match_location, :before,
    :after, :line_number, :status_code, :path,
    :reporter, :reward, :jira_ids, :payload, :app, :negative,
    :fingerprint, :commit_email, :commit_name, :commit_branch, :match_count, :regex,
    :reopened, :vul_class



  def initialize
    @attack_vectors=[]
    @identified = Time.now
    @status = "New"
    @id = SecureRandom.hex
  end

  def is_valid?
    true
  end

  def self.open_statuses
    ["new", "ticketed", "awaiting validation"]
  end

  def self.closed_statuses
    ["ignored", "false positive", "remediated", "risk accepted"]
  end


end

class AttackVector

  attr_accessor :type, :name, :method, :payload, :note

  def initialize

  end


end

Result.class_eval do

  before_save :vulnerability_counter
  def vulnerability_counter
    if self.changes.include? "metadata"
      # puts self.changes
      if self.changes.try(:[], "metadata").try(:first).try(:[], "vulnerabilities").nil?
        return
      end
      unless (self.changes.try(:[], "metadata").try(:first).try(:[], "vulnerabilities").to_a - self.changes.try(:[], "metadata").try(:last).try(:[], "vulnerabilities").to_a).blank?
        self.update_vulnerabilities([], false)
      end
    end
    return true
  end

  def update_sitemap(sitemap=[], source_name=nil)

    self.metadata["sitemap"] ||= []

    sitemap.each do |url|

      entry = self.metadata["sitemap"].select{|entry| entry["url"] == url}.first
      if(!entry)
        entry = {}
        entry["url"] = url
        entry["identified"] = entry["last_seen"] = Time.now
        begin
          uri = URI.parse(url)
          entry["scheme"] = uri.scheme
          entry["host"] = uri.host

          path = uri.path.split("/")
          if(path.last.to_s.include?("."))
            entry["endpoint"] = path.pop
          end

          entry["path"] = path.blank? ? "/" : path.join("/")
          entry["source"] = source_name if source_name

        rescue
        end
        self.metadata["sitemap"] << entry
      else
        entry["last_seen"] = Time.now
        entry["source"] ||= source_name if source_name
      end
    end



  end

  # Add a set of vulnerabilities to the
  def add_scan_vulnerabilities(vulnerabilities, sitemap, source_name, task_id, auto_remediate=false, options={})

    updated_ids, vulnerability_statuses = update_vulnerabilities(vulnerabilities, false, false, options)
    vulnerability_statuses["closed"] ||= 0
    update_sitemap(sitemap, source_name) if sitemap

    if(auto_remediate && open_vulnerabilities.present?)

      open_vulnerabilities.select{|v| v["task_id"].to_s == task_id.to_s && !updated_ids.include?(v["id"])}.each{|v|
        v["status"] = "Remediated"

        vulnerability_statuses["closed"] += 1
      }
    end

    self.metadata["scan_metadata"] ||= []
    self.metadata["scan_metadata"] << {parsed: Time.now, source: source_name, task_id: task_id }.merge(vulnerability_statuses)



    update_vulnerability_counts

    update_trends


    self.save


    return vulnerability_statuses

  end

  def update_vulnerabilities(vulnerabilities=[], save_result=true, update_counts=true, options={})

    vulnerability_status = {"existing" => 0, "new"=> 0, "reopened"=> 0,"regression"=> 0, "closed"=>0}
    updated_ids = []
    self.metadata ||={}
    self.metadata["vulnerabilities"] ||=[]

    self.metadata["vulnerabilities"].each do |vuln|
      if(!Vulnerability.open_statuses.include?(vuln["status"].to_s.downcase) && !Vulnerability.closed_statuses.include?(vuln["status"].to_s.downcase))
        if(vuln["jira_ids"].present?)
          vuln["status"] = "Ticketed"
        else
          vuln["status"] = "New"
        end
      end
      unless vuln.try(:[], "jira_ids").to_s == ""
        vuln["jira_ids"] = vuln["jira_ids"].gsub(/\s+/, "").upcase
        if(Vulnerability.open_statuses.include?(vuln["status"].to_s.downcase))
          vuln["status"] = "Ticketed"
        end
      end
    end

    vulnerabilities.each do |v|

      res = find_duplicate_vulnerability(v, options)

      if(res.present?)
        if options[:update_all_matching].nil? ||  options[:update_all_matching] == false
         res = [res.first]
        end

        res.each do |r|
          r["id"] ||= SecureRandom.hex
          updated_ids << r["id"]
          r["source"] ||= []
          if(!r["source"].include?(v.source))
            r["source"] << v.source
            r["source"].uniq!
          end

          if(v.jira_ids.present?)
            if(r["jira_ids"].present?)
              r["jira_ids"] += ",#{v.jira_ids}"
            else
              r["jira_ids"] = v.jira_ids
            end
            r["jira_ids"] = r["jira_ids"].split(",").uniq.join(",")
          end

          r["external_link"] ||={}
          r["external_link"].merge(v.external_link || {})

          v.attack_vectors.each do |vector|
            r["attack_vectors"] ||=[]
            existing_vector = find_duplicate_attack_vector(r["attack_vectors"], v, vector)
            if(existing_vector.blank?)
              r["attack_vectors"] << JSON.parse(vector.to_json)
            end
          end
          if(!Vulnerability.closed_statuses.include?(v.status.to_s.downcase))

            if(r["status"].to_s.match /Remediated/)
              # Mark "Remediated" or "Auto Remediated" as Reopened
              vulnerability_status["regression"] += 1
              r["reopened"] = Time.now


              # elsif(r["status"] == "False Positive" && (Time.now - Time.parse(r["identified"])) > 30.days)
              #   # Reopen False Positives that are re-identified more than 30 days later
              #   # TODO: Make this logic configurable
              #   vulnerability_status["reopened"] += 1
              #   r["status"] = "Reopened"
              #   r["identified"] = Time.now
            else
              vulnerability_status["existing"] += 1
            end
            # For any closed statuses except remediated we'll do nothing
            # If it's remediated we want to reopen the vulnerability
            # If it's already open we want to update the status to reflect whether there is a ticket
            if(!Vulnerability.closed_statuses.except("remediated").include?(r["status"].to_s.downcase))
              if(r["jira_ids"].present?)
                r["status"] = "Ticketed"
              else
                r["status"] = "New"
              end
            end
          else
            if(!Vulnerability.closed_statuses.include?(r["status"].to_s.downcase))
              vulnerability_status["closed"] += 1
            end

            r["status"] = v.status
            if(!Vulnerability.open_statuses.include?(r["status"].to_s.downcase) && !Vulnerability.closed_statuses.include?(r["status"].to_s.downcase))
              if(r["jira_ids"].present?)
                r["status"] = "Ticketed"
              else
                r["status"] = "New"
              end
            end
          end

          r["reporter"] = v.reporter if v.reporter
          r["reward"] = v.reward if v.reward

          r["fingerprint"] = v.fingerprint if v.fingerprint

          r["vul_class"] = v.vul_class if v.vul_class

          # Do not want to overwrite existing details
          # r["details"] = v.details
        end

      else
        vulnerability_status["new"] += 1
        v.identified = Time.now
        v.id = SecureRandom.hex
        updated_ids << v.id
        v.severity = v.severity.to_s.titleize
        if(v.source.class == String || v.source.class == Array)
          v.source = Array(v.source).uniq
        else
          v.source = [v.source].uniq
        end
        if(!Vulnerability.open_statuses.include?(v.status.to_s.downcase) && !Vulnerability.closed_statuses.include?(v.status.to_s.downcase))
          if(v.jira_ids.present?)
            v.status = "Ticketed"
          else
            v.status = "New"
          end
        end

        self.metadata["vulnerabilities"] << JSON.parse(v.to_json)

      end
    end

    update_vulnerability_counts if update_counts



    #puts self.inspect
    if save_result
      self.save
    end

    return [updated_ids, vulnerability_status]

  end

  def update_vulnerability_counts

    task_counts = Hash.new(0)
    source_counts = Hash.new(0)
    status_counts = Hash.new(0)
    key_suffix_counts = Hash.new(0)


    self.metadata["vulnerabilities"] ||= []

    open_vulnerabilities = self.open_vulnerabilities
    closed_vulnerabilities = self.metadata["vulnerabilities"].select{|v| Vulnerability.closed_statuses.include?(v["status"].to_s.downcase) }
    closed_count = closed_vulnerabilities.count

    new_count = self.metadata["vulnerabilities"].select{|v| v["status"].to_s.match(/open/i) }.count

    informational_count = self.metadata["vulnerabilities"].select{|v| v["status"].to_s.match(/open/i) && v["severity"].to_s.downcase == "informational"}.count

    severity_counts = Hash.new(0).merge({"critical" => 0, "high" => 0, "medium" =>0, "low" => 0, "informational" => informational_count, "observational" => 0})

    state_counts = {"open"=> open_vulnerabilities.length, "closed"=>closed_count}

    open_vulnerabilities.each { |v|
      if(v["key_suffix"].present?)
        key_suffix_counts[v["key_suffix"]] += 1
      end
      if(v["task_id"].present?)
        task_counts[v["task_id"]] += 1
      end
      if(v["source"].present? && v["source"].class == Array)
        v["source"].each do |s|
          source_counts[s] += 1
        end
      end
      if(v["severity"].present?)
        severity_counts[v["severity"].to_s.downcase] += 1
      end
      if(v["status"].present?)
        status_counts[v["status"].to_s.downcase] += 1
      end
    }

    closed_vulnerabilities.each {|v|
      if(v["status"].present?)
        status_counts[v["status"].to_s.downcase] += 1
      end

    }

    self.metadata["vulnerability_count"] ||={}
    self.metadata["vulnerabilities"] ||=[]
    self.metadata["vulnerability_count"]["severity"] = severity_counts
    self.metadata["vulnerability_count"]["key_suffix"] = key_suffix_counts
    self.metadata["vulnerability_count"]["task_id"] = task_counts
    self.metadata["vulnerability_count"]["source"] = source_counts
    self.metadata["vulnerability_count"]["status"] = status_counts
    self.metadata["vulnerability_count"]["state"] = state_counts
    return

  end

  def update_trends
    self.metadata["trends"] ||={"open_vulnerabilities" => {"data" =>[{"name"=> "open","data"=>{},"library"=>{"steppedLine"=>true}  }]}}
    self.metadata["trends"]["open_vulnerabilities"]["data"].first["data"][Time.now.strftime("%b %d %Y %H:%M:%S")] = self.open_vulnerabilities.count
  end

  def open_vulnerabilities
    begin
      self.metadata["vulnerabilities"].select{|v| Vulnerability.open_statuses.include?(v["status"].to_s.downcase) && v["severity"].to_s.downcase != "informational"}
    rescue
      []
    end
  end

  def auto_remediate(task_id, options={})
    if(task_id.present? && self.metadata.try(:[],"vulnerabilities").present?)
      results = self.metadata["vulnerabilities"].select{|v| v["task_id"] == task_id}
      options.each do |key,value|
        results = results.select{|v| v[key] == value}
      end
      result.each {|v| v["status"] = "Remediated"}
    end
  end

  def auto_remediate(task_id, url, term=nil, payload=nil)
    if self.metadata.try(:[],"vulnerabilities").present?
      if term.to_s == "" and payload.to_s == ""
        self.metadata["vulnerabilities"].each_with_object({}) do |vuln|
          if vuln["url"] == url && vuln["task_id"] == task_id

            vuln["status"] = "Remediated"
          end
        end
      elsif payload.to_s == ""
        self.metadata["vulnerabilities"].each_with_object({}) do |vuln|
          if vuln["url"] == url && vuln["task_id"] == task_id && vuln["term"] == term
            vuln["status"] = "Remediated"
          end
        end
      elsif term.to_s == ""
        self.metadata["vulnerabilities"].each_with_object({}) do |vuln|
          if vuln["url"] == url && vuln["task_id"] == task_id && vuln["payload"] == payload
            vuln["status"] = "Remediated"
          end
        end
      else
        self.metadata["vulnerabilities"].each_with_object({}) do |vuln|
          if vuln["url"] == url && vuln["task_id"] == task_id && vuln["term"] == term && vuln["payload"] == payload
            vuln["status"] = "Remediated"
          end
        end
      end
    end

    if self.changed?
      self.save!
    end
  end


  def find_duplicate_vulnerability(vulnerability, options={})
    # locations: content, path, headers
    # code_fragment

    # For some tasks, like curl we don't want to find duplicates if they came from other tasks.
    if(options.try(:[],:isolate_vulnerabilities) == true)
      existing_vulnerabilities = self.metadata["vulnerabilities"].select{|v| v["task_id"] == vulnerability.task_id}
    else
      existing_vulnerabilities = self.metadata["vulnerabilities"]
    end

    case vulnerability.match_location
    when "content"
      # This is a static analyzer match for content
      return existing_vulnerabilities.select{|v| v["url"] == vulnerability.url && v["term"] == vulnerability.term }
    when "file"
      # This is a static analyzer match for file and github event monitor
      return existing_vulnerabilities.select{|v| v["file_name"] == vulnerability.file_name && v["term"] == vulnerability.term }
    when "source_code"
      # This is a static analyzer match for any static analyzer which has
      # source_code_line, source_code_file, and type
      return existing_vulnerabilities.select{|v| v["source_code_file"] == vulnerability.source_code_file && v["type"] == vulnerability.type}
    when "path"
      # This is a static analyzer match for paths
      return existing_vulnerabilities.select{|v| v["url"] == vulnerability.url && v["term"] == vulnerability.term}
    when "headers"
      # This matches curl analyzers running on headers
      return existing_vulnerabilities.select{|v| v["url"] == vulnerability.url && v["term"] == vulnerability.term}
    when "fingerprint"
      # This is the Rails analyzer fingerprint matcher with fallback
      return existing_vulnerabilities.select do |v|
        if v["url"] != vulnerability.url
          false
        elsif v["fingerprint"] && v["fingerprint"] == vulnerability.fingerprint
          true
        else
          # Fall back on heuristics until fingerprint updated
          ["severity", "type", "source_code_file", "source_code_line"].all? do |data|
            v[data] == vulnerability.send(data.to_sym)
          end
        end
      end
    else
      if vulnerability.url
        vuln_url = URI.parse(URI.encode(vulnerability.url))
        if !vuln_url.query.nil?
          return existing_vulnerabilities.select{|v|
              v["type"] == vulnerability.type &&
              vuln_url.host == URI.parse(v["url"]).host &&
              vuln_url.path == URI.parse(v["url"]).path &&
              !URI.parse(v["url"]).query.nil? &&
              URI.parse(v["url"]).query.split('&').each_slice(1).map(&:join).map { |param| param.split('=').first }.sort == vuln_url.query.split('&').each_slice(1).map(&:join).map { |param| param.split('=').first }.sort}
        else
          return existing_vulnerabilities.select{|v|
          v["type"] == vulnerability.type && v["url"] == vulnerability.url}
        end
      elsif vulnerability.jira_ids
        return existing_vulnerabilities.select{|v|
        v["jira_ids"].present? && v["jira_ids"].split(",").include?(vulnerability.jira_ids)}
      end
    end
  end

  def find_duplicate_attack_vector(vector_metadata, vulnerability, vulnerability_vector)
    vector_metadata.select{|v| v["type"] == vulnerability_vector.type && v["name"] == vulnerability_vector.name }
  end

end
