\FF\D8\FF\E0\00JFIF\00\00\00d\00d\00\00\FF\FE\00\border bs:0 bc:#000000 ps:0 pc:#ffffff es:0 ec:#000000 ck:feee6c715d26fd9f38b0ca4278c05026\FF\DB\00C\00P7<F<2PFAFZUP_xxnnx\F5\AF\B9\91\C8\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\DB\00CUZZxix‚\EB\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\FF\C0\00\00b\00d\00\FF\C4\00\00\00\00\00\00\00\00\00\00\00\00\00\00\FF\C4\00\00\00\00\00\00\00\00\00\00\001A!Q\FF\C4\00\00\00\00\00\00\00\00\00\00\00\00\00\FF\C4\00\00\00\00\00\00\00\00\00\00!1AQ\FF\DA\00\00\00?\00\F6\00\00\00\00\00\00\00\00\00\00\A0\00\00\C5\CF\F8\E7Ó\FCjD~\B9^\AD\%\B3]\D8cs-\BBs,-\A0\00"\80\00%\B83rÏ^K~F\A4ea\00E\00\C6\EE=u\B1\9B\D1\00@\00r\BE8\F9:\FE5#.M\00\E7\CB\C9q\CBq\D6\F2\BE\B7\C7>\C9n5×\D6?\BDê\9Ds\EBp\9F[`8m\B7)o\B5\E8\E6I\99\FE3]]A2\BA\8Cw\D6E\93\\DEv\C8\009\F2\F1NI?uc\\F5\EA\96k\xN<~buv\EA\C8\D7	\8B\84\CEcxI\BBg\AE\9E=\D6+n\EC\80\C8A\8C\AE\EB\CF\D5\DA\E9"2\A4\B9j5\EB\F3W\B63\96\B30Yu\DA\FC8\ED\DF\E7Ms\FB\F1\8E\B3\FA\EA\E8\E6(\883zs\F2_\8DFk\8Bh
\00\8C\DCw\D3R\B5+6X\BA\B2\C4j\AB0\B4\FCMw\C2I\8E\9B\E3\A9~9u\FA\D3l\80\C8%p\EE\FDn2€
\00$\FEj\C4e\A9\DB\~\95\A7\A5\80EK\BB\8DDsP\00@@AD'k\CF\E8\DB\D2(\80\9AK\D3\85\D6lb\F2\BA\8C*\80\00)\95
59\A3R:\F3\CE"\B6\80\88\00\00i1u4ê\E9\F2á\A6\A2\FACM\93WMb*\E0*\00\00\00\00\00(\A8\80\00\00\80\00\00\00\00\00\00\00\00\00\FF\D9<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>C///</title>
</head>
#
# Copyright 2017, Christoph Hartmann
#

require "inspec/resources/command"
require "inspec/utils/filter"
require "hashie/mash"

module Inspec::Resources
  class DockerContainerFilter
    # use filtertable for containers
    filter = FilterTable.create
    filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
    filter.register_column(:commands, field: "command")
      .register_column(:ids,            field: "id")
      .register_column(:images,         field: "image")
      .register_column(:labels,         field: "labels", style: :simple)
      .register_column(:local_volumes,  field: "localvolumes")
      .register_column(:mounts,         field: "mounts")
      .register_column(:names,          field: "names")
      .register_column(:networks,       field: "networks")
      .register_column(:ports,          field: "ports")
      .register_column(:running_for,    field: "runningfor")
      .register_column(:sizes,          field: "size")
      .register_column(:status,         field: "status")
      .register_custom_matcher(:running?) do |x|
        x.where { status.downcase.start_with?("up") }
      end
    filter.install_filter_methods_on_resource(self, :containers)

    attr_reader :containers
    def initialize(containers)
      @containers = containers
    end
  end

  class DockerImageFilter
    filter = FilterTable.create
    filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
    filter.register_column(:ids, field: "id")
      .register_column(:repositories,  field: "repository")
      .register_column(:tags,          field: "tag")
      .register_column(:sizes,         field: "size")
      .register_column(:digests,       field: "digest")
      .register_column(:created,       field: "createdat")
      .register_column(:created_since, field: "createdsize")
    filter.install_filter_methods_on_resource(self, :images)

    attr_reader :images
    def initialize(images)
      @images = images
    end
  end

  class DockerPluginFilter
    filter = FilterTable.create
    filter.add(:ids, field: "id")
      .add(:names,    field: "name")
      .add(:versions, field: "version")
      .add(:enabled,  field: "enabled")
    filter.connect(self, :plugins)

    attr_reader :plugins
    def initialize(plugins)
      @plugins = plugins
    end
  end

  class DockerServiceFilter
    filter = FilterTable.create
    filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
    filter.register_column(:ids, field: "id")
      .register_column(:names,    field: "name")
      .register_column(:modes,    field: "mode")
      .register_column(:replicas, field: "replicas")
      .register_column(:images,   field: "image")
      .register_column(:ports,    field: "ports")
    filter.install_filter_methods_on_resource(self, :services)

    attr_reader :services
    def initialize(services)
      @services = services
    end
  end

  # This resource helps to parse information from the docker host
  # For compatability with Serverspec we also offer the following resouses:
  # - docker_container
  # - docker_image
  class Docker < Inspec.resource(1)
    name "docker"
    supports platform: "unix"
    desc "
      A resource to retrieve information about docker
    "

    example <<~EXAMPLE
      describe docker.containers do
        its('images') { should_not include 'u12:latest' }
      end

      describe docker.images do
        its('repositories') { should_not include 'inssecure_image' }
      end

      describe docker.plugins.where { name == 'rexray/ebs' } do
        it { should exist }
      end

      describe docker.services do
        its('images') { should_not include 'inssecure_image' }
      end

      describe docker.version do
        its('Server.Version') { should cmp >= '1.12'}
        its('Client.Version') { should cmp >= '1.12'}
      end

      describe docker.object(id) do
        its('Configuration.Path') { should eq 'value' }
      end

      docker.containers.ids.each do |id|
        # call docker inspect for a specific container id
        describe docker.object(id) do
          its(%w(HostConfig Privileged)) { should cmp false }
          its(%w(HostConfig Privileged)) { should_not cmp true }
        end
      end
    EXAMPLE

    def containers
      DockerContainerFilter.new(parse_containers)
    end

    def images
      DockerImageFilter.new(parse_images)
    end

    def plugins
      DockerPluginFilter.new(parse_plugins)
    end

    def services
      DockerServiceFilter.new(parse_services)
    end

    def version
      return @version if defined?(@version)

      data = {}
      cmd = inspec.command("docker version --format '{{ json . }}'")
      data = JSON.parse(cmd.stdout) if cmd.exit_status == 0
      @version = Hashie::Mash.new(data)
    rescue JSON::ParserError => _e
      Hashie::Mash.new({})
    end

    def info
      return @info if defined?(@info)

      data = {}
      # docke info format is only supported for Docker 17.03+
      cmd = inspec.command("docker info --format '{{ json . }}'")
      data = JSON.parse(cmd.stdout) if cmd.exit_status == 0
      @info = Hashie::Mash.new(data)
    rescue JSON::ParserError => _e
      Hashie::Mash.new({})
    end

    # returns information about docker objects
    def object(id)
      return @inspect if defined?(@inspect)

      data = JSON.parse(inspec.command("docker inspect #{id}").stdout)
      data = data[0] if data.is_a?(Array)
      @inspect = Hashie::Mash.new(data)
    rescue JSON::ParserError => _e
      Hashie::Mash.new({})
    end

    def to_s
      "Docker Host"
    end

    private

    def parse_json_command(labels, subcommand)
      # build command
      format = labels.map { |label| "\"#{label}\": {{json .#{label}}}" }
      raw = inspec.command("docker #{subcommand} --format '{#{format.join(", ")}}'").stdout
      output = []
      # since docker is not outputting valid json, we need to parse each row
      raw.each_line do |entry|
        # convert all keys to lower_case to work well with ruby and filter table
        row = JSON.parse(entry).map do |key, value|
          [key.downcase, value]
        end.to_h

        # ensure all keys are there
        row = ensure_keys(row, labels)

        # strip off any linked container names
        # Depending on how it was linked, the actual container name may come before
        # or after the link information, so we'll just look for the first name that
        # does not include a slash since that is not a valid character in a container name
        if row["names"]
          row["names"] = row["names"].split(",").find { |c| !c.include?("/") }
        end

        # Split labels on ',' or set to empty array
        # Allows for `docker.containers.where { labels.include?('app=redis') }`
        row["labels"] = row.key?("labels") ? row["labels"].split(",") : []

        output.push(row)
      end

      output
    rescue JSON::ParserError => _e
      warn "Could not parse `docker #{subcommand}` output"
      []
    end

    def parse_containers
      # @see https://github.com/moby/moby/issues/20625, works for docker 1.13+
      # raw_containers = inspec.command('docker ps -a --no-trunc --format \'{{ json . }}\'').stdout
      # therefore we stick with older approach
      labels = %w{Command CreatedAt ID Image Lab