<commit_before>require 'image-inspector-client'
require 'kubeclient'

class ManageIQ::Providers::Kubernetes::ContainerManager::Scanning::Job < Job
  PROVIDER_CLASS = ManageIQ::Providers::Kubernetes::ContainerManager
  INSPECTOR_NAMESPACE = 'management-infra'
  INSPECTOR_PORT = 8080
  DOCKER_SOCKET = '/var/run/docker.sock'
  SCAN_CATEGORIES = %w(system software)
  POD_POLL_INTERVAL = 10
  IMAGES_GUEST_OS = 'Linux'
  INSPECTOR_HEALTH_PATH = '/healthz'
  ERRCODE_POD_NOTFOUND = 404

  def load_transitions
    self.state ||= 'initializing'
    {
      :initializing => {'initializing'     => 'waiting_to_start'},
      :start        => {'waiting_to_start' => 'pod_create'},
      :pod_wait     => {'pod_create'       => 'waiting_to_scan'},
      :analyze      => {'waiting_to_scan'  => 'scanning'},
      :data         => {'scanning'      => 'synchronizing',
                        'synchronizing' => 'synchronizing'},
      :cleanup      => {'synchronizing'    => 'pod_delete'},
      :abort_job    => {'*'                => 'aborting'},
      :finish       => {'pod_delete' => 'finished',
                        'aborting'   => 'finished'},
    }
  end

  def initializing
    queue_signal(:start)
  end

  def start
    image = target_entity
    return queue_signal(:abort_job, "no image found", "error") unless image
    return queue_signal(:abort_job, "cannont analyze non-docker images", "error") unless image.docker_id

    ems_configs = VMDB::Config.new('vmdb').config[:ems]

    namespace = ems_configs.fetch_path(:ems_kubernetes, :miq_namespace)
    namespace = INSPECTOR_NAMESPACE if namespace.blank?

    update!(:options => options.merge(
      :ems_id          => image.ext_management_system.id,
      :docker_image_id => image.docker_id,
      :image_full_name => image.full_name,
      :pod_name        => "manageiq-img-scan-#{image.docker_id[0..11]}",
      :pod_port        => INSPECTOR_PORT,
      :pod_namespace   => namespace
    ))

    pod = pod_definition
    _log.info("creating pod #{pod_full_name} to analyze docker image " \
              "#{options[:docker_image_id]}: #{pod.to_json}")

    begin
      kubernetes_client.create_pod(pod)
    rescue SocketError, KubeException => e
      msg = "pod creation for #{pod_full_name} failed: #{e}"
      _log.info(msg)
      return queue_signal(:abort_job, msg, "error")
    end

    queue_signal(:pod_wait)
  end

  def pod_wait
    _log.info("waiting for pod #{pod_full_name} to be available")

    client       = kubernetes_client
    health_url   = pod_proxy_url(client, INSPECTOR_HEALTH_PATH)
    http_options = {
      :use_ssl     => health_url.scheme == 'https',
      :verify_mode => ext_management_system.verify_ssl_mode
    }

    # TODO: move this to a more appropriate place (lib)
    loop do
      response = pod_health_poll(client, health_url, http_options)

      case response
      when Net::HTTPOK
        _log.info("pod #{pod_full_name} is ready and accessible")
        break
      when Net::HTTPServiceUnavailable
        # TODO: check that the pod wasn't terminated (exit code)
        # continue: pod is still not up and running
      else
        msg = "unknown access error to pod #{pod_full_name}: #{response}"
        _log.info(msg)
        return queue_signal(:abort_job, msg, "error")
      end

      # TODO: for recovery purposes it would be better if this
      # method was short-lived instead of waiting for the pod to be
      # available
      _log.info("pod #{pod_full_name} is not available")
      sleep(POD_POLL_INTERVAL)
    end

    queue_signal(:analyze)
  end

  def analyze
    image = target_entity
    return queue_signal(:abort_job, "no image found", "error") unless image

    _log.info("scanning image #{options[:docker_image_id]}")

    scan_args = {
      :pod_namespace => options[:pod_namespace],
      :pod_name      => options[:pod_name],
      :pod_port      => options[:pod_port],
      :guest_os      => IMAGES_GUEST_OS
    }

    actual = image_inspector_client.fetch_metadata.Id
    if actual != options[:docker_image_id]
      msg = "cannot analyze image %s with id %s: detected id was %s"
      _log.error(msg % [options[:image_full_name], options[:docker_image_id], actual])
      return queue_signal(:abort_job,
                          msg % [options[:image_full_name], options[:docker_image_id][0..11], actual[0..11]],
                          'error')
    end

    collect_compliance_data(image)

    image.scan_metadata(SCAN_CATEGORIES,
                        "taskid" => jobid,
                        "host"   => MiqServer.my_server,
                        "args"   => [YAML.dump(scan_args)])
  end

  def collect_compliance_data(image)
    return unless image_inspector_client.respond_to? :fetch_oscap_arf
    openscap_result = image.openscap_result || OpenscapResult.new(:container_image => image)
    openscap_result.attach_raw_result(image_inspector_client.fetch_oscap_arf)<commit_msg>Log before collecting compliance data<commit_after>require 'image-inspector-client'
require 'kubeclient'

class ManageIQ::Providers::Kubernetes::ContainerManager::Scanning::Job < Job
  PROVIDER_CLASS = ManageIQ::Providers::Kubernetes::ContainerManager
  INSPECTOR_NAMESPACE = 'management-infra'
  INSPECTOR_PORT = 8080
  DOCKER_SOCKET = '/var/run/docker.sock'
  SCAN_CATEGORIES = %w(system software)
  POD_POLL_INTERVAL = 10
  IMAGES_GUEST_OS = 'Linux'
  INSPECTOR_HEALTH_PATH = '/healthz'
  ERRCODE_POD_NOTFOUND = 404

  def load_transitions
    self.state ||= 'initializing'
    {
      :initializing => {'initializing'     => 'waiting_to_start'},
      :start        => {'waiting_to_start' => 'pod_create'},
      :pod_wait     => {'pod_create'       => 'waiting_to_scan'},
      :analyze      => {'waiting_to_scan'  => 'scanning'},
      :data         => {'scanning'      => 'synchronizing',
                        'synchronizing' => 'synchronizing'},
      :cleanup      => {'synchronizing'    => 'pod_delete'},
      :abort_job    => {'*'                => 'aborting'},
      :finish       => {'pod_delete' => 'finished',
                        'aborting'   => 'finished'},
    }
  end

  def initializing
    queue_signal(:start)
  end

  def start
    image = target_entity
    return queue_signal(:abort_job, "no image found", "error") unless image
    return queue_signal(:abort_job, "cannont analyze non-docker images", "error") unless image.docker_id

    ems_configs = VMDB::Config.new('vmdb').config[:ems]

    namespace = ems_configs.fetch_path(:ems_kubernetes, :miq_namespace)
    namespace = INSPECTOR_NAMESPACE if namespace.blank?

    update!(:options => options.merge(
      :ems_id          => image.ext_management_system.id,
      :docker_image_id => image.docker_id,
      :image_full_name => image.full_name,
      :pod_name        => "manageiq-img-scan-#{image.docker_id[0..11]}",
      :pod_port        => INSPECTOR_PORT,
      :pod_namespace   => namespace
    ))

    pod = pod_definition
    _log.info("creating pod #{pod_full_name} to analyze docker image " \
              "#{options[:docker_image_id]}: #{pod.to_json}")

    begin
      kubernetes_client.create_pod(pod)
    rescue SocketError, KubeException => e
      msg = "pod creation for #{pod_full_name} failed: #{e}"
      _log.info(msg)
      return queue_signal(:abort_job, msg, "error")
    end

    queue_signal(:pod_wait)
  end

  def pod_wait
    _log.info("waiting for pod #{pod_full_name} to be available")

    client       = kubernetes_client
    health_url   = pod_proxy_url(client, INSPECTOR_HEALTH_PATH)
    http_options = {
      :use_ssl     => health_url.scheme == 'https',
      :verify_mode => ext_management_system.verify_ssl_mode
    }

    # TODO: move this to a more appropriate place (lib)
    loop do
      response = pod_health_poll(client, health_url, http_options)

      case response
      when Net::HTTPOK
        _log.info("pod #{pod_full_name} is ready and accessible")
        break
      when Net::HTTPServiceUnavailable
        # TODO: check that the pod wasn't terminated (exit code)
        # continue: pod is still not up and running
      else
        msg = "unknown access error to pod #{pod_full_name}: #{response}"
        _log.info(msg)
        return queue_signal(:abort_job, msg, "error")
      end

      # TODO: for recovery purposes it would be better if this
      # method was short-lived instead of waiting for the pod to be
      # available
      _log.info("pod #{pod_full_name} is not available")
      sleep(POD_POLL_INTERVAL)
    end

    queue_signal(:analyze)
  end

  def analyze
    image = target_entity
    return queue_signal(:abort_job, "no image found", "error") unless image

    _log.info("scanning image #{options[:docker_image_id]}")

    scan_args = {
      :pod_namespace => options[:pod_namespace],
      :pod_name      => options[:pod_name],
      :pod_port      => options[:pod_port],
      :guest_os      => IMAGES_GUEST_OS
    }

    actual = image_inspector_client.fetch_metadata.Id
    if actual != options[:docker_image_id]
      msg = "cannot analyze image %s with id %s: detected id was %s"
      _log.error(msg % [options[:image_full_name], options[:docker_image_id], actual])
      return queue_signal(:abort_job,
                          msg % [options[:image_full_name], options[:docker_image_id][0..11], actual[0..11]],
                          'error')
    end

    collect_compliance_data(image)

    image.scan_metadata(SCAN_CATEGORIES,
                        "taskid" => jobid,
                        "host"   => MiqServer.my_server,
                        "args"   => [YAML.dump(scan_args)])
  end

  def collect_compliance_data(image)
    return unless image_inspector_client.respond_to? :fetch_oscap_arf
    _log.info "collecting compliance data for #{options[:docker_image_id]}"
    openscap_result = image.openscap_result || OpenscapResult.new(:container_image => image)
    openscap_result.attach_raw_result(image_inspector_client.fetch_oscap_arf)
{'fraction_non_alphanumeric': 0.11332665330661322, 'fraction_numerical': 0.0036072144288577155, 'mean_word_length': 2.8686046511627907, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4156068', 'n_tokens_mistral': 3246, 'n_tokens_neox': 3113, 'n_words': 655}