# encoding: utf-8 # # Copyright 2017, Christoph Hartmann # # author: Christoph Hartmann # author: Patrick Muench # author: Dominik Richter require 'utils/filter' require 'hashie/mash' module Inspec::Resources class DockerContainerFilter # use filtertable for containers filter = FilterTable.create filter.add_accessor(:where) .add_accessor(:entries) .add(:commands, field: 'command') .add(:ids, field: 'id') .add(:images, field: 'image') .add(:labels, field: 'labels') .add(:local_volumes, field: 'localvolumes') .add(:mounts, field: 'mounts') .add(:names, field: 'names') .add(:networks, field: 'networks') .add(:ports, field: 'ports') .add(:running_for, field: 'runningfor') .add(:sizes, field: 'size') .add(:status, field: 'status') .add(:exists?) { |x| !x.entries.empty? } .add(:running?) { |x| x.where { status.downcase.start_with?('up') } } filter.connect(self, :containers) attr_reader :containers def initialize(containers) @containers = containers end end class DockerImageFilter filter = FilterTable.create filter.add_accessor(:where) .add_accessor(:entries) .add(:ids, field: 'id') .add(:repositories, field: 'repository') .add(:tags, field: 'tag') .add(:sizes, field: 'size') .add(:digests, field: 'digest') .add(:created, field: 'createdat') .add(:created_since, field: 'createdsize') .add(:exists?) { |x| !x.entries.empty? } filter.connect(self, :images) attr_reader :images def initialize(images) @images = images 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) # rubocop:disable Metrics/ClassLength name 'docker' desc " A resource to retrieve information about docker " 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.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 " def containers DockerContainerFilter.new(parse_containers) end def images DockerImageFilter.new(parse_images) 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 return 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 return 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 return Hashie::Mash.new({}) end def to_s 'Docker Host' end private 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 Labels Mounts Names Ports RunningFor Size Status} # Networks LocalVolumes work with 1.13+ only if !version.empty? && Gem::Version.new(version['Client']['Version']) >= Gem::Version.new('1.13') labels.push('Networks') labels.push('LocalVolumes') end # build command format = labels.map { |label| "\"#{label}\": {{json .#{label}}}" } raw_containers = inspec.command("docker ps -a --no-trunc --format '{#{format.join(', ')}}'").stdout ps = [] # since docker is not outputting valid json, we need to parse each row raw_containers.each_line { |entry| j = JSON.parse(entry) # convert all keys to lower_case to work well with ruby and filter table j = j.map { |k, v| [k.downcase, v] }.to_h # ensure all keys are there j = ensure_container_keys(j) # 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 j['names'] = j['names'].split(',').find { |c| !c.include?('/') } ps.push(j) } ps rescue JSON::ParserError => _e warn 'Could not parse `docker ps` output' [] end def ensure_container_keys(entry) %w{Command CreatedAt ID Image Labels Mounts Names Ports RunningFor Size Status Networks LocalVolumes}.each { |key| entry[key.downcase] = nil if !entry.key?(key.downcase) } entry end def parse_images # docker does not support the `json .` function here, therefore we need to emulate that behavior. raw_images = inspec.command('docker images -a --no-trunc --format \'{ "id": {{json .ID}}, "repository": {{json .Repository}}, "tag": {{json .Tag}}, "size": {{json .Size}}, "digest": {{json .Digest}}, "createdat": {{json .CreatedAt}}, "createdsize": {{json .CreatedSince}} }\'').stdout c_images = [] raw_images.each_line { |entry| c_images.push(JSON.parse(entry)) } c_images rescue JSON::ParserError => _e warn 'Could not parse `docker images` output' [] end end end