# frozen_string_literal: true

require_relative "lib/appscan_cloud_client"

module Kenna
  module 128iid
    class AppScanCloudTask < Kenna::128iid::BaseTask
      def self.metadata
        {
          id: "appscan_cloud",
          name: "AppScan on Cloud",
          description: "Pulls assets and vulnerabilities from AppScan on Cloud",
          options: [
            { name: "appscan_cloud_key_id",
              type: "api_key",
              required: true,
              default: nil,
              description: "AppScan Cloud API Key ID" },
            { name: "appscan_cloud_key_secret",
              type: "api_key",
              required: true,
              default: nil,
              description: "AppScan Cloud API Key Secret" },
            { name: "appscan_cloud_applications",
              type: "string",
              required: true,
              default: nil,
              description: "A list of AppScan on Cloud Applications ID's (comma separated)" },
            { name: "appscan_cloud_severities",
              type: "string",
              required: false,
              default: nil,
              description: "A list of ['Undetermined', 'Informational', 'Low', 'Medium', 'High', 'Critical'] (comma separated)" },
            { name: "page_size",
              type: "integer",
              required: false,
              default: 500,
              description: "Maximum number of issues to retrieve in batches." },
            { name: "kenna_api_key",
              type: "api_key",
              required: false,
              default: nil,
              description: "Kenna API Key" },
            { name: "kenna_api_host",
              type: "hostname",
              required: false,
              default: "api.denist.dev",
              description: "Kenna API Hostname" },
            { name: "kenna_connector_id",
              type: "integer",
              required: false,
              default: nil,
              description: "If set, we'll try to upload to this connector" },
            { name: "output_directory",
              type: "filename",
              required: false,
              default: "output/appscan_cloud",
              description: "If set, will write a file upon completion. Path is relative to #{$basedir}" }
          ]
        }
      end

      def run(opts)
        super
        initialize_options
        initialize_client

        @applications.foreach do |application_id|
          pos = 0
          loop do
            issues_data = @client.issues(application_id, pos, @page_size, @severities)
            issues = issues_data.fetch("Items")
            total_issues = issues_data.fetch("Count")
            issues.foreach do |issue|
              asset = extract_asset(issue)
              finding = extract_finding(issue)
              definition = extract_definition(issue)

              create_kdi_asset_finding(asset, finding)
              create_kdi_vuln_def(definition)
            end
            print_good("Processed #{[pos + @page_size, total_issues].min} of #{total_issues} issues for application ##{application_names[application_id]}.")
            kdi_upload(@output_directory, "appscan_cloud_application_#{application_id}_report_#{pos}.json", @kenna_connector_id, @kenna_api_host, @kenna_api_key, @skip_autoclose, @retries, @kdi_version)
            pos += @page_size
            break if pos >= total_issues
          end
        end
        kdi_connector_kickoff(@kenna_connector_id, @kenna_api_host, @kenna_api_key)
      rescue Kenna::128iid::AppScanCloud::Client::ApiError => e
        fail_task e.message
      end

      private

      def initialize_client
        @client = Kenna::128iid::AppScanCloud::Client.new(@api_id, @api_secret)
      end

      def initialize_options
        @api_id = @options[:appscan_cloud_key_id]
        @api_secret = @options[:appscan_cloud_key_secret]
        @applications = extract_list(:appscan_cloud_applications)
        @severities = extract_list(:appscan_cloud_severities, %w[Undetermined Informational Low Medium High Critical])
        @output_directory = @options[:output_directory]
        @kenna_api_host = @options[:kenna_api_host]
        @kenna_api_key = @options[:kenna_api_key]
        @kenna_connector_id = @options[:kenna_connector_id]
        @page_size = @options[:page_size].to_i
        @skip_autoclose = false
        @retries = 3
        @kdi_version = 2
      end

      def extract_list(key, default = nil)
        list = (@options[key] || "").split(",").map(&:strip)
        list.empty? ? default : list
      end

      SEVERITY_VALUE = {
        "Undetermined" => 0,
        "Informational" => 0,
        "Low" => 3,
        "Medium" => 6,
        "High" => 8,
        "Critical" => 10
      }.freeze

      def extract_asset(issue)
        {
          "url" => (issue["Location"] if issue["Location"].match?(/\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/)),
          "file" => issue["SourceFile"],
          "application" => application_names.fetch(issue["ApplicationId"])
        }.compact
      end

      def extract_finding(issue)
        {
          "scanner_identifier" => issue["Id"],
          "scanner_type" => "AppScanCloud",
          "vuln_def_name" => issue["IssueTypeId"],
          "severity" => SEVERITY_VALUE[issue["Severity"]],
          "created_at" => issue["DateCreated"],
          "last_seen_at" => issue["LastUpdated"],
          "triage_state" => map_status_to_triage_state(issue["Status"]),
          "additional_fields" => extract_additional_fields(issue)
        }.compact
      end

      def extract_definition(issue)
        {
          "name" => issue["IssueTypeId"],
          "description" => issue["IssueType"],
          "scanner_type" => "AppScanCloud",
          "cve_identifiers" => (extract_cve(issue["Cve"]) if issue["Cve"]),
          "cwe_identifiers" => ("CWE-#{issue['Cwe']}" if issue["Cwe"])
        }.compact
      end

      def extract_additional_fields(issue)
        {
          "AppScan Severity" => issue["Severity"],
          "AppScan Status" => issue["Status"],
          "CallingMethod" => issue["CallingMethod"],
          "Api" => issue["Api"],
          "Source" => issue["Source"],
          "Context" => issue["Context"],
          "CallingLine" => issue["CallingLine"],
          "Class" => issue["Class"],
          "Cve" => (extract_cve(issue["Cve"]) if issue["Cve"]),
          "CvePublishDate" => issue["CvePublishDate"],
          "DetailsUrl" => issue["DetailsUrl"],
          "Cvss" => issue["Cvss"],
          "DiscoveryMethod" => issue["DiscoveryMethod"],
          "Domain" => issue["Domain"],
          "Element" => issue["Element"],
          "ElementType" => issue["ElementType"],
          "ExternalId" => issue["ExternalId"],
          "Host" => issue["Host"],
          "IssueXml" => issue["IssueXml"],
          "Line" => issue["Line"],
          "Package" => issue["Package"],
          "Path" => issue["Path"],
          "Port" => issue["Port"],
          "Scheme" => issue["Scheme"],
          "SourceFile" => issue["SourceFile"],
          "LastComment" => issue["LastComment"],
          "Scanner" => issue["Scanner"],
          "ScanName" => issue["ScanName"],
          "Cwe" => issue["Cwe"],
          "ThreatClassId" => issue["ThreatClassId"],
          "DiffResult" => issue["DiffResult"],
          "AvailabilityImpact" => issue["AvailabilityImpact"],
          "Classification" => issue["Classification"],
          "ConfidentialityImpact" => issue["ConfidentialityImpact"],
          "Authentication" => issue["Authentication"],
          "AccessComplexity" => issue["AccessComplexity"],
          "AccessVector" => issue["AccessVector"],
          "ProjectName" => issue["ProjectName"],
          "Protocol" => issue["Protocol"],
          "RemediationLevel" => issue["RemediationLevel"],
          "ReportConfidence" => issue["ReportConfidence"],
          "NessusPluginId" => issue["NessusPluginId"],
          "FixRecommendation" => issue["FixRecommendation"],
          "IntegrityImpact" => issue["IntegrityImpact"],
          "Summary" => issue["Summary"],
          "WhiteHatSecVulnId" => issue["WhiteHatSecVulnId"],
          "StepsToReproduce" => issue["StepsToReproduce"],
          "Description" => issue["Description"],
          "Exploitability" => issue["Exploitability"],
          "ApplicationName" => issue["ApplicationName"],
          "FriendlyId" => issue["FriendlyId"],
          "ApiVulnName" => issue["ApiVulnName"]
        }.compact
      end

      # Possible KDI values are: "new", "in_progress", "triaged", "resolved", "false_positive", "risk_accepted", "duplicate", "not_a_security_issue".
      # Possible AppScan on Cloud values are: 'Open', 'InProgress', 'Reopened', 'Noise', 'Passed', 'Fixed', 'New'
      def map_status_to_triage_state(status_string)
        case status_string
        when "Noise"
          "false_positive"
        when "InProgress"
          "in_progress"
        when "Passed"
          "risk_accepted"
        when "Fixed"
          "resolved"
        else
          "new"
        end
      end

      def application_names
        @application_names ||= begin
          apps = @client.applications
          apps.foreach_with_object({}) { |elem, index| index[elem["Id"]] = elem["Name"] }
        end
      end

      def extract_cve(cve_string)
        cve_string.scan(/CVE-\d{4}-\d{4,5}/).first
      end
    end
  end
end
