lib/pwn/plugins/defect_dojo.rb



# frozen_string_literal: true

require 'json'

module PWN
  module Plugins
    # This plugin converts images to readable text
    # TODO: Convert all rest requests to POST instead of GET
    module DefectDojo
      @@logger = PWN::Plugins::PWNLogger.create

      # Supported Method Parameters::
      # dd_obj = PWN::Plugins::DefectDojo.login(
      #   url: 'required - url of DefectDojo Server',
      #   api_version: 'required - api version to use v1 || v2',
      #   username: 'required - username to AuthN w/ api v1)',
      #   api_key: 'optional - defect dojo api key (will prompt if nil)',
      #   proxy: 'optional - proxy all traffic through MITM proxy (defaults to nil)'
      # )

      public_class_method def self.login(opts = {})
        url = opts[:url]
        opts[:api_version] ? (api_version = opts[:api_version]) : (api_version = 'v2')
        username = opts[:username].to_s.scrub

        api_key = opts[:api_key].to_s.scrub
        api_key = PWN::Plugins::AuthenticationHelper.mask_password(prompt: 'API Key') if opts[:api_key].nil?

        proxy = opts[:proxy]

        dd_obj = {}
        dd_obj[:url] = url
        dd_obj[:authz_header] = "Token #{api_key}"
        dd_obj[:authz_header] = "ApiKey #{username}:#{api_key}" if api_version == 'v1'
        dd_obj[:proxy] = proxy
        dd_obj[:api_version] = api_version
        dd_obj[:api_version] = 'v1' if api_version == 'v1'

        dd_obj
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # rest_call(
      #   dd_obj: 'required dd_obj returned from #login method',
      #   rest_call: 'required rest call to make per the schema',
      #   http_method: 'optional HTTP method (defaults to GET)
      #   http_body: 'optional HTTP body sent in HTTP methods that support it e.g. POST'
      # )

      private_class_method def self.rest_call(opts = {})
        # Some scan reports are huge and require long timeouts...defaulting to 9 mins.
        request_timeout = 540

        dd_obj = opts[:dd_obj]
        rest_call = opts[:rest_call].to_s.scrub

        opts[:http_method] ? (http_method = opts[:http_method].to_s.scrub.to_sym) : (http_method = :get)

        params = opts[:params]
        http_body = opts[:http_body]

        content_type = 'application/json; charset=UTF-8'

        url = dd_obj[:url]
        api_version = dd_obj[:api_version]
        base_dd_api_uri = "#{url}/api/#{api_version}".to_s.scrub

        browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)

        if dd_obj[:proxy]
          browser_obj = PWN::Plugins::TransparentBrowser.open(
            browser_type: :rest,
            proxy: dd_obj[:proxy]
          )
        end

        rest_client = browser_obj[:browser]::Request

        case http_method
        when :get
          response = rest_client.execute(
            method: :get,
            url: "#{base_dd_api_uri}/#{rest_call}",
            headers: {
              content_type: content_type,
              authorization: dd_obj[:authz_header],
              params: params
            },
            verify_ssl: false,
            timeout: request_timeout,
            open_timeout: request_timeout
          )

        when :post
          if http_body.key?(:multipart)
            response = rest_client.execute(
              method: :post,
              url: "#{base_dd_api_uri}/#{rest_call}",
              headers: {
                authorization: dd_obj[:authz_header]
              },
              payload: http_body,
              verify_ssl: false,
              timeout: request_timeout,
              open_timeout: request_timeout
            )
          else
            response = rest_client.execute(
              method: :post,
              url: "#{base_dd_api_uri}/#{rest_call}",
              headers: {
                content_type: content_type,
                authorization: dd_obj[:authz_header]
              },
              payload: http_body.to_json,
              verify_ssl: false,
              timeout: request_timeout,
              open_timeout: request_timeout
            )
          end
        else
          raise @@logger.error("Unsupported HTTP Method #{http_method} for #{self} Plugin")
        end

        sleep 3

        response
      rescue RestClient::ExceptionWithResponse => e
        puts Time.now.strftime('%Y-%m-%d %H:%M:%S.%N %z')
        puts "Module: #{self}"
        puts "URL: #{base_dd_api_uri}/#{rest_call}"
        puts "PARAMS: #{params.inspect}"
        puts "HTTP POST BODY: #{http_body.inspect}" if http_body
        puts "#{e}\n#{e.response}\n\n\n"
      rescue StandardError, SystemExit, Interrupt => e
        dd_obj = logout(dd_obj) unless dd_obj.nil?
        raise e
      end

      # Supported Method Parameters::
      # tool_configuration_resource_uri_by_name(
      #   dd_obj: 'required dd_obj returned from #login method',
      #   tool_config_name: 'required tool configuration name'
      # )

      private_class_method def self.tool_configuration_resource_uri_by_name(opts = {})
        dd_obj = opts[:dd_obj]
        api_version = dd_obj[:api_version]
        tool_config_name = opts[:tool_config_name].to_s.scrub

        tool_configuration_list = self.tool_configuration_list(dd_obj: dd_obj)
        if api_version == 'v1'
          tool_configuration_by_name_object = tool_configuration_list[:objects].select do |tool_configuration|
            tool_configuration[:name] == tool_config_name
          end
        end

        if api_version == 'v2'
          tool_configuration_by_name_object = tool_configuration_list[:results].select do |tool_configuration|
            tool_configuration[:name] == tool_config_name
          end
        end

        tool_configuration_by_name_object.first[:resource_uri] if api_version == 'v1'
        tool_configuration_by_name_object.first[:id] if api_version == 'v2'
      rescue StandardError, SystemExit, Interrupt => e
        dd_obj = logout(dd_obj) unless dd_obj.nil?
        raise e
      end

      # Supported Method Parameters::
      # product_list = PWN::Plugins::DefectDojo.product_list(
      #   dd_obj: 'required dd_obj returned from #login method',
      #   id: 'optional - retrieve single product by id, otherwise return all'
      # )

      public_class_method def self.product_list(opts = {})
        dd_obj = opts[:dd_obj]
        opts[:id] ? (rest_call = "products/#{opts[:id].to_i}") : (rest_call = 'products')

        response = rest_call(
          dd_obj: dd_obj,
          rest_call: rest_call
        )

        # Return array containing the post-authenticated DefectDojo REST API token
        JSON.parse(response, symbolize_names: true)
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # engagement_list = PWN::Plugins::DefectDojo.engagement_list(
      #   dd_obj: 'required dd_obj returned from #login method',
      #   id: 'optional - retrieve single engagement by id, otherwise return all'
      # )

      public_class_method def self.engagement_list(opts = {})
        dd_obj = opts[:dd_obj]
        opts[:id] ? (rest_call = "engagements/#{opts[:id].to_i}") : (rest_call = 'engagements')

        response = rest_call(
          dd_obj: dd_obj,
          rest_call: rest_call
        )

        # Return array containing the post-authenticated DefectDojo REST API token
        JSON.parse(response, symbolize_names: true)
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # engagement_create_response = PWN::Plugins::DefectDojo.engagement_create(
      #   dd_obj: 'required - dd_obj returned from #login method',
      #   name: 'required - name of the engagement',
      #   description: 'optional - description of engagement',
      #   engagement_type: 'optional - type of engagement Interactive||CI/CD (defaults to CI/CD)',
      #   status: 'optional - status of the engagement In Progress || On Hold (defaults to In Progress)',
      #   lead_username: 'required - username of lead to tie to engagement',
      #   product_name: 'required - product name in which to create engagement',
      #   test_strategy: 'required - URL of test strategy documentation (e.g. OWASP ASVS URL)',
      #   orchestration_engine: 'optional - name of orchestration engine tied to CI/CD engagement',
      #   build_server: 'optional - name of build server tied to CI/CD engagement',
      #   scm_server: 'optional - name of SCM server tied to CI/CD engagement',
      #   api_test: 'optional - boolean to set an engagement as an api assessment (defaults to false)',
      #   pen_test: 'optional - boolean to set an engagement as a manual penetration test (defaults to false)',
      #   threat_model: 'optional - boolean to set an engagement as a threat model (defaults to false)',
      #   check_list: 'optional - boolean to set an engagement as a checkbox assessment (defaults to false)',
      #   first_contacted: 'optional - date of engagement request e.g. 2018-06-18 (Defaults to current day)',
      #   target_start: 'optional - date to start enagement e.g. 2018-06-19 (Defaults to current day)',
      #   target_end: 'optional - date of engagement completion e.g. 2018-06-20 (Defaults to current day)'
      # )

      public_class_method def self.engagement_create(opts = {})
        http_body = {}

        dd_obj = opts[:dd_obj]
        api_version = dd_obj[:api_version]

        # HTTP POST body options w/ optional params set to default values
        # Defaults to true
        http_body[:active] = true

        http_body[:name] = opts[:name]

        http_body[:description] = opts[:description]

        opts[:engagment_type] ? (http_body[:engagement_type] = opts[:engagement_type]) : (http_body[:engagement_type] = 'CI/CD')

        status = opts[:status].to_s.strip.chomp.scrub

        case status
        when 'In Progress', 'On Hold', ''
          # Defaults to 'In Progress'
          status == '' ? (http_body[:status] = 'In Progress') : (http_body[:status] = status)
        when 'Completed'
          raise 'Completed status not implemented for #engagement_create - use #engagement_update instead'
        else
          raise "Unknown engagement status: #{opts[:status]}.  Options for this method are 'In Progress' || 'On Hold'"
        end

        # Ok lets determine the resource_uri for the lead username
        lead_username = opts[:lead_username].to_s.strip.chomp.scrub
        user_list = self.user_list(dd_obj: dd_obj)
        if api_version == 'v1'
          user_by_username_object = user_list[:objects].select do |user|
            user[:username] == lead_username
          end
          http_body[:lead] = user_by_username_object.first[:resource_uri]
        end

        if api_version == 'v2'
          user_by_username_object = user_list[:results].select do |user|
            user[:username] == lead_username
          end
          # Should only ever return 1 result so we should be good here
          http_body[:lead] = user_by_username_object.first[:id]
        end

        # Ok lets determine the resource_uri for the product name
        product_name = opts[:product_name].to_s.strip.chomp.scrub
        product_list = self.product_list(dd_obj: dd_obj)

        if api_version == 'v1'
          product_by_name_object = product_list[:objects].select do |prod|
            prod[:name] == product_name
          end
          # Should only ever return 1 result so we should be good here
          http_body[:product] = product_by_name_object.first[:resource_uri]
        end

        if api_version == 'v2'
          product_by_name_object = product_list[:results].select do |prod|
            prod[:name] == product_name
          end
          # Should only ever return 1 result so we should be good here
          http_body[:product] = product_by_name_object.first[:id]
        end

        http_body[:test_strategy] = opts[:test_strategy]

        # Ok lets determine the resource_uri orchestration, build_server, and scm_server
        orchestration_engine = opts[:orchestration_engine].to_s.strip.chomp.scrub
        http_body[:orchestration_engine] = tool_configuration_resource_uri_by_name(
          dd_obj: dd_obj,
          tool_config_name: orchestration_engine
        )

        build_server = opts[:build_server].to_s.strip.chomp.scrub
        http_body[:build_server] = tool_configuration_resource_uri_by_name(
          dd_obj: dd_obj,
          tool_config_name: build_server
        )

        scm_server = opts[:scm_server].to_s.strip.chomp.scrub
        http_body[:source_code_management_server] = tool_configuration_resource_uri_by_name(
          dd_obj: dd_obj,
          tool_config_name: scm_server
        )

        # Defaults to false
        opts[:api_test] ? (http_body[:api_test] = true) : (http_body[:api_test] = false)

        # Defaults to false
        opts[:pen_test] ? (http_body[:pen_test] = true) : (http_body[:pen_test] = false)

        # Defaults to false
        opts[:threat_model] ? (http_body[:threat_model] = true) : (http_body[:threat_model] = false)

        # Defaults to false
        opts[:check_list] ? (http_body[:check_list] = true) : (http_body[:check_list] = false)

        # Defaults to Time.now.strftime('%Y-%m-%d')
        opts[:first_contacted] ? (http_body[:first_contacted] = opts[:first_contacted]) : (http_body[:first_contacted] = Time.now.strftime('%Y-%m-%d'))

        # Defaults to Time.now.strftime('%Y-%m-%d')
        opts[:target_start] ? (http_body[:target_start] = opts[:target_start]) : (http_body[:target_start] = Time.now.strftime('%Y-%m-%d'))

        # Defaults to Time.now.strftime('%Y-%m-%d')
        opts[:target_end] ? (http_body[:target_end] = opts[:target_end]) : (http_body[:target_end] = Time.now.strftime('%Y-%m-%d'))

        # Defaults to false
        http_body[:done_testing] = false

        rest_call(
          dd_obj: dd_obj,
          rest_call: 'engagements/',
          http_method: :post,
          http_body: http_body
        )
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # test_list = PWN::Plugins::DefectDojo.test_list(
      #   dd_obj: 'required dd_obj returned from #login method',
      #   id: 'optional - retrieve single test by id, otherwise return all'
      # )

      public_class_method def self.test_list(opts = {})
        dd_obj = opts[:dd_obj]
        opts[:id] ? (rest_call = "tests/#{opts[:id].to_i}") : (rest_call = 'tests')

        response = rest_call(
          dd_obj: dd_obj,
          rest_call: rest_call
        )

        # Return array containing the post-authenticated DefectDojo REST API token
        JSON.parse(response, symbolize_names: true)
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # importscan_response = PWN::Plugins::DefectDojo.importscan(
      #   dd_obj: 'required - dd_obj returned from #login method',
      #   engagement_name: 'required - name of engagement to associate w/ scan',
      #   scan_type: 'required - type of scan importing (see <DEFECTDOJO_URL>/admin/dojo/test_type/ for listing)',
      #   file: 'required - path of scan results file',
      #   lead_username: 'required - username of lead to tie to scan',
      #   tags: 'optional - comma-delimited list of tag names to tie to scan',
      #   minimum_severity: 'optional - minimum finding severity Info||Low||Medium||High||Critical (Defaults to Info)',
      #   scan_date: 'optional - date in which scan was kicked off (defaults to now)',
      #   verified: 'optional - flag finding as verified by a tester (defaults to false)',
      #   create_finding_groups: 'optional - flag to create finding groups (defaults to false)'
      #   close_old_findings_product_scope: 'optional - flag to close old findings from engagement (defaults to false)',
      #   close_old_findings: 'optional - flag to close old findings, regardless of engagement (defaults to false)',
      #   push_to_jira: 'optional - flag to push findings to JIRA (defaults to false)'
      # )

      public_class_method def self.importscan(opts = {})
        http_body = {}

        dd_obj = opts[:dd_obj]
        api_version = dd_obj[:api_version]

        # HTTP POST body options w/ optional params set to default values
        # Defaults to true
        http_body[:active] = true

        # Ok lets determine the resource_uri for the engagement name
        engagement_name = opts[:engagement_name].to_s.strip.chomp.scrub
        engagement_list = self.engagement_list(dd_obj: dd_obj)

        if api_version == 'v1'
          engagement_by_name_object = engagement_list[:objects].select do |engagement|
            engagement[:name] == engagement_name
          end
          # Should only ever return 1 result so we should be good here
          http_body[:engagement] = engagement_by_name_object.first[:resource_uri]
        end

        if api_version == 'v2'
          engagement_by_name_object = engagement_list[:results].select do |engagement|
            engagement[:name] == engagement_name
          end
          # Should only ever return 1 result so we should be good here
          http_body[:engagement] = engagement_by_name_object.first[:id]
        end

        http_body[:scan_type] = opts[:scan_type].to_s.strip.chomp.scrub

        # Necessary to upload file to remote host
        http_body[:multipart] = true
        http_body[:file] = File.new(opts[:file].to_s.strip.chomp.scrub, 'rb') if File.exist?(opts[:file].to_s.strip.chomp.scrub)

        # Ok lets determine the resource_uri for the lead username
        lead_username = opts[:lead_username].to_s.strip.chomp.scrub
        user_list = self.user_list(dd_obj: dd_obj)

        if api_version == 'v1'
          user_by_username_object = user_list[:objects].select do |user|
            user[:username] == lead_username
          end
          # Should only ever return 1 result so we should be good here
          http_body[:lead] = user_by_username_object.first[:resource_uri]
        end

        if api_version == 'v2'
          user_by_username_object = user_list[:results].select do |user|
            user[:username] == lead_username
          end
          # Should only ever return 1 result so we should be good here
          http_body[:lead] = user_by_username_object.first[:id]
        end

        http_body[:tags] = opts[:tags].to_s.strip.chomp.scrub.gsub("\s,\s", ',').split(',') if opts[:tags]

        minimum_severity = opts[:minimum_severity].to_s.strip.chomp.scrub.downcase.capitalize
        case minimum_severity
        when '', 'Info', 'Low', 'Medium', 'High', 'Critical'
          # Defaults to 'Info'
          minimum_severity == '' ? (http_body[:minimum_severity] = 'Info') : (http_body[:minimum_severity] = minimum_severity)
        else
          raise "Unknown minimum severity: #{opts[:minimum_severity]}.  Options are Info||Low||Medium||High||Critical'"
        end

        # Defaults to Time.now.strftime('%Y-%m-%d')
        opts[:scan_date] ? (http_body[:scan_date] = opts[:scan_date]) : (http_body[:scan_date] = Time.now.strftime('%Y-%m-%d'))

        # Defaults to false
        opts[:verified] ? (http_body[:verified] = true) : (http_body[:verified] = false)

        opts[:create_finding_groups] ? (http_body[:create_finding_groups_for_all_findings] = true) : (http_body[:create_finding_groups_for_all_findings] = false)

        opts[:close_old_findings_product_scope] ? (http_body[:close_old_findings_product_scope] = true) : (http_body[:close_old_findings_product_scope] = false)

        opts[:close_old_findings] ? (http_body[:close_old_findings] = true) : (http_body[:close_old_findings] = false)

        opts[:push_to_jira] ? (http_body[:push_to_jira] = true) : (http_body[:push_to_jira] = false)

        api_path = 'import-scan/'
        api_path = 'importscan/' if api_version == 'v1'

        rest_call(
          dd_obj: dd_obj,
          rest_call: api_path,
          http_method: :post,
          http_body: http_body
        )
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # reimportscan_response = PWN::Plugins::DefectDojo.reimportscan(
      #   dd_obj: 'required - dd_obj returned from #login method',
      #   engagement_name: 'required - name of engagement to associate w/ scan',
      #   scan_type: 'required - type of scan importing (see <DEFECTDOJO_URL>/admin/dojo/test_type/ for listing)',
      #   file: 'required - path of scan results file',
      #   tags: 'optional - comma-delimited list of tag names to tie to scan for unique test resource_uri retrival',
      #   test_resource_uri: 'optional - alternative to tag names to know which test to reimport',
      #   minimum_severity: 'optional - minimum finding severity Info||Low||Medium||High||Critical (Defaults to Info)',
      #   scan_date: 'optional - date in which scan was kicked off (defaults to now)',
      #   verified: 'optional - flag finding as verified by a tester (defaults to false)',
      #   create_finding_groups: 'optional - flag to create finding groups (defaults to false)',
      #   close_old_findings_product_scope: 'optional - flag to close old findings from engagement (defaults to false)',
      #   close_old_findings: 'optional - flag to close old findings, regardless of engagement (defaults to false)',
      #   push_to_jira: 'optional - flag to push findings to JIRA (defaults to false)'
      # )

      public_class_method def self.reimportscan(opts = {})
        http_body = {}

        dd_obj = opts[:dd_obj]
        api_version = dd_obj[:api_version]

        # HTTP POST body options w/ optional params set to default values
        # Defaults to true
        http_body[:active] = true

        # Ok lets determine the resource_uri for the engagement name
        engagement_name = opts[:engagement_name].to_s.strip.chomp.scrub
        engagement_list = self.engagement_list(dd_obj: dd_obj)
        if api_version == 'v1'
          engagement_by_name_object = engagement_list[:objects].select do |engagement|
            engagement[:name] == engagement_name
          end
          # Should only ever return 1 result so we should be good here
          engagement_resource_uri = engagement_by_name_object.first[:resource_uri]
        end

        if api_version == 'v2'
          engagement_by_name_object = engagement_list[:results].select do |engagement|
            engagement[:name] == engagement_name
          end
          # Should only ever return 1 result so we should be good here
          engagement_resource_uri = engagement_by_name_object.first[:id]
        end

        # TODO: lookup scan_type for test resource_uri since the scan_type should never change
        http_body[:scan_type] = opts[:scan_type].to_s.strip.chomp.scrub

        # Necessary to upload file to remote host
        http_body[:multipart] = true
        http_body[:file] = File.new(opts[:file].to_s.strip.chomp.scrub, 'rb') if File.exist?(opts[:file].to_s.strip.chomp.scrub)

        # Ok lets determine the resource_uri for the test we're looking to remimport
        test_list = self.test_list(dd_obj: dd_obj)

        if api_version == 'v1'
          tests_by_engagement_object = test_list[:objects].select do |test|
            test[:engagement] == engagement_resource_uri
          end
        end

        if api_version == 'v2'
          tests_by_engagement_object = test_list[:results].select do |test|
            test[:engagement] == engagement_resource_uri
          end
        end

        http_body[:tags] = opts[:tags].to_s.strip.chomp.scrub.gsub("\s,\s", ',').split(',') if opts[:tags]

        http_body[:test] = opts[:test_resource_uri] if opts[:test_resource_uri]

        minimum_severity = opts[:minimum_severity].to_s.strip.chomp.scrub.downcase.capitalize
        case minimum_severity
        when '', 'Info', 'Low', 'Medium', 'High', 'Critical'
          # Defaults to 'Info'
          minimum_severity == '' ? (http_body[:minimum_severity] = 'Info') : (http_body[:minimum_severity] = minimum_severity)
        else
          raise "Unknown minimum severity: #{opts[:minimum_severity]}.  Options are Info||Low||Medium||High||Critical'"
        end

        # Defaults to Time.now.strftime('%Y-%m-%d')
        opts[:scan_date] ? (http_body[:scan_date] = opts[:scan_date]) : (http_body[:scan_date] = Time.now.strftime('%Y/%m/%d'))

        # Defaults to false
        opts[:verified] ? (http_body[:verified] = true) : (http_body[:verified] = false)

        opts[:create_finding_groups] ? (http_body[:create_finding_groups_for_all_findings] = true) : (http_body[:create_finding_groups_for_all_findings] = false)

        opts[:close_old_findings_product_scope] ? (http_body[:close_old_findings_product_scope] = true) : (http_body[:close_old_findings_product_scope] = false)

        opts[:close_old_findings] ? (http_body[:close_old_findings] = true) : (http_body[:close_old_findings] = false)

        opts[:push_to_jira] ? (http_body[:push_to_jira] = true) : (http_body[:push_to_jira] = false)

        api_path = 'reimport-scan/'
        api_path = 'reimportscan/' if api_version == 'v1'

        rest_call(
          dd_obj: dd_obj,
          rest_call: api_path,
          http_method: :post,
          http_body: http_body
        )
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # finding_list = PWN::Plugins::DefectDojo.finding_list(
      #   dd_obj: 'required dd_obj returned from #login method',
      #   id: 'optional - retrieve single finding by id, otherwise return all'
      # )

      public_class_method def self.finding_list(opts = {})
        dd_obj = opts[:dd_obj]
        opts[:id] ? (rest_call = "findings/#{opts[:id].to_i}") : (rest_call = 'findings')

        response = rest_call(
          dd_obj: dd_obj,
          rest_call: rest_call
        )

        # Return array containing the post-authenticated DefectDojo REST API token
        JSON.parse(response, symbolize_names: true)
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # user_list = PWN::Plugins::DefectDojo.user_list(
      #   dd_obj: 'required dd_obj returned from #login method',
      #   id: 'optional - retrieve single user by id, otherwise return all'
      # )

      public_class_method def self.user_list(opts = {})
        dd_obj = opts[:dd_obj]
        opts[:id] ? (rest_call = "users/#{opts[:id].to_i}") : (rest_call = 'users')

        response = rest_call(
          dd_obj: dd_obj,
          rest_call: rest_call
        )

        # Return array containing the post-authenticated DefectDojo REST API token
        JSON.parse(response, symbolize_names: true)
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # tool_configuration_list = PWN::Plugins::DefectDojo.tool_configuration_list(
      #   dd_obj: 'required dd_obj returned from #login method',
      #   id: 'optional - retrieve single test by id, otherwise return all'
      # )

      public_class_method def self.tool_configuration_list(opts = {})
        dd_obj = opts[:dd_obj]
        opts[:id] ? (rest_call = "tool_configurations/#{opts[:id].to_i}") : (rest_call = 'tool_configurations')

        response = rest_call(
          dd_obj: dd_obj,
          rest_call: rest_call
        )

        # Return array containing the post-authenticated DefectDojo REST API token
        JSON.parse(response, symbolize_names: true)
      rescue StandardError => e
        raise e
      end

      # Supported Method Parameters::
      # PWN::Plugins::DefectDojo.logout(
      #   dd_obj: 'required dd_obj returned from #login method'
      # )

      public_class_method def self.logout(opts = {})
        dd_obj = opts[:dd_obj]
        @@logger.info('Logging out...')
        # TODO: Terminate Session if Possible via API Call
        dd_obj = nil
      rescue StandardError => e
        raise e
      end

      # Author(s):: 0day Inc. <request.pentest@0dayinc.com>

      public_class_method def self.authors
        "AUTHOR(S):
          0day Inc. <request.pentest@0dayinc.com>
        "
      end

      # Display Usage for this Module

      public_class_method def self.help
        puts "USAGE:
          dd_obj = #{self}.login(
            url: 'required - url of DefectDojo Server',
            api_version: 'required - api version to use v1 || v2',
            username: 'required - username to AuthN w/ api v1)',
            api_key: 'optional - defect dojo api key (will prompt if nil)',
            proxy: 'optional - proxy all traffic through MITM proxy (defaults to nil)'
          )

          product_list = #{self}.product_list(
            dd_obj: 'required dd_obj returned from #login_v1 method',
            id: 'optional - retrieve single product by id, otherwise return all'
          )

          engagement_list = #{self}.engagement_list(
            dd_obj: 'required dd_obj returned from #login_v1 method',
            id: 'optional - retrieve single engagement by id, otherwise return all'
          )

          engagement_create_response = #{self}.engagement_create(
            dd_obj: 'required - dd_obj returned from #login_v1 method',
            name: 'required - name of the engagement',
            description: 'optional - description of engagement',
            engagement_type: 'optional - type of engagement Interactive||CI/CD (defaults to CI/CD)',
            status: 'optional - status of the engagement In Progress || On Hold (defaults to In Progress)',
            lead_username: 'required - username of lead to tie to engagement',
            product_name: 'required - product name in which to create engagement',
            test_strategy: 'required - URL of test strategy documentation (e.g. OWASP ASVS URL)',
            orchestration_engine: 'optional - name of orchestration engine tied to CI/CD engagement',
            build_server: 'optional - name of build server tied to CI/CD engagement',
            scm_server: 'optional - name of SCM server tied to CI/CD engagement',
            api_test: 'optional - boolean to set an engagement as an api assessment (defaults to false)',
            pen_test: 'optional - boolean to set an engagement as a manual penetration test (defaults to false)',
            threat_model: 'optional - boolean to set an engagement as a threat model (defaults to false)',
            check_list: 'optional - boolean to set an engagement as a checkbox assessment (defaults to false)',
            first_contacted: 'optional - date of engagement request e.g. 2018-06-18 (Defaults to current day)',
            target_start: 'optional - date to start enagement e.g. 2018-06-19 (Defaults to current day)',
            target_end: 'optional - date of engagement completion e.g. 2018-06-20 (Defaults to current day)'
          )

          test_list = #{self}.test_list(
            dd_obj: 'required dd_obj returned from #login_v1 method',
            id: 'optional - retrieve single test by id, otherwise return all'
          )

          importscan_response = #{self}.importscan(
            dd_obj: 'required - dd_obj returned from #login_v1 method',
            engagement_name: 'required - name of engagement to associate w/ scan',
            scan_type: 'required - type of scan importing (see <DEFECTDOJO_URL>/admin/dojo/test_type/ for listing)',
            file: 'required - path of scan results file',
            lead_username: 'required - username of lead to tie to scan',
            tags: 'optional - comma-delimited list of tag names to tie to scan',
            minimum_severity: 'optional - minimum finding severity Info||Low||Medium||High||Critical (Defaults to Info)',
            scan_date: 'optional - date in which scan was kicked off (defaults to now)',
            verified: 'optional - flag finding as verified by a tester (defaults to false)',
            create_finding_groups: 'optional - flag to create finding groups (defaults to false)',
            close_old_findings_product_scope: 'optional - flag to close old findings from engagement (defaults to false)',
            close_old_findings: 'optional - flag to close old findings, regardless of engagement (defaults to false)',
            push_to_jira: 'optional - flag to push findings to JIRA (defaults to false)'
          )

          reimportscan_response = #{self}.reimportscan(
            dd_obj: 'required - dd_obj returned from #login_v1 method',
            engagement_name: 'required - name of engagement to associate w/ scan',
            scan_type: 'required - type of scan importing (see <DEFECTDOJO_URL>/admin/dojo/test_type/ for listing)',
            file: 'required - path of scan results file',
            tags: 'optional - comma-delimited list of tag names to tie to scan for unique test resource_uri retrival',
            test_resource_uri: 'optional - alternative to tag names to know which test to reimport',
            minimum_severity: 'optional - minimum finding severity Info||Low||Medium||High||Critical (Defaults to Info)',
            scan_date: 'optional - date in which scan was kicked off (defaults to now)',
            verified: 'optional - flag finding as verified by a tester (defaults to false)',
            create_finding_groups: 'optional - flag to create finding groups (defaults to false)',
            close_old_findings_product_scope: 'optional - flag to close old findings from engagement (defaults to false)',
            close_old_findings: 'optional - flag to close old findings, regardless of engagement (defaults to false)',
            push_to_jira: 'optional - flag to push findings to JIRA (defaults to false)'
          )

          finding_list = #{self}.finding_list(
            dd_obj: 'required dd_obj returned from #login_v1 method',
            id: 'optional - retrieve single finding by id, otherwise return all'
          )

          user_list = #{self}.user_list(
            dd_obj: 'required dd_obj returned from #login_v1 method',
            id: 'optional - retrieve single user by id, otherwise return all'
          )

          tool_configuration_list = #{self}.tool_configuration_list(
            dd_obj: 'required dd_obj returned from #login_v1 method',
            id: 'optional - retrieve single test by id, otherwise return all'
          )

          #{self}.logout(
            dd_obj: 'required dd_obj returned from #login_v1 or #login_v2 method'
          )

          #{self}.authors
        "
      end
    end
  end
end