diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..6100c69 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,11 @@ +Style/FrozenStringLiteralComment: + Enabled: false + +Style/Documentation: + Enabled: false + +Metrics/MethodLength: + Max: 50 + +Metrics/BlockLength: + Max: 200 diff --git a/bin/check-container-vulnerabilities.rb b/bin/check-container-vulnerabilities.rb index 927078b..f90f575 100755 --- a/bin/check-container-vulnerabilities.rb +++ b/bin/check-container-vulnerabilities.rb @@ -44,8 +44,9 @@ class CheckContainerVulnerabilities < Sensu::Plugin::Check::CLI proc: proc { |w| w.split(',') } def run - status, message = Quayio::Scanner::Check.new( - config[:docker_url], config[:quayio_token], config[:whitelist]).run + status, message = Quayio::Scanner::Check.new(config[:docker_url], + config[:quayio_token], + config[:whitelist]).run if status == :ok ok message diff --git a/lib/quayio/scanner.rb b/lib/quayio/scanner.rb index f93f57d..2ef5fa3 100644 --- a/lib/quayio/scanner.rb +++ b/lib/quayio/scanner.rb @@ -1,5 +1,7 @@ -require 'quayio/scanner/version' require 'quayio/scanner/check' +require 'quayio/scanner/image' +require 'quayio/scanner/repository' +require 'quayio/scanner/version' module Quayio module Scanner diff --git a/lib/quayio/scanner/check.rb b/lib/quayio/scanner/check.rb index 651b6a7..0379266 100644 --- a/lib/quayio/scanner/check.rb +++ b/lib/quayio/scanner/check.rb @@ -1,19 +1,10 @@ -require 'quayio/scanner/image' require 'docker' module Quayio module Scanner - class Check < Struct.new(:docker_url, :quayio_token, :whitelist) + Check = Struct.new(:docker_url, :quayio_token, :whitelist) do def run Docker.url = docker_url - containers = Docker::Container.all - .map { |dc| dc.json['Config']['Image'] } - .uniq - - vulnerable_images = containers - .map { |container| Image.new(container, quayio_token, whitelist) } - .select(&:vulnerable?) - .map(&:name) if vulnerable_images.empty? [:ok, "#{containers.size} Containers are ok"] @@ -21,6 +12,22 @@ def run [:critical, "The images are insecure: #{vulnerable_images.join(', ')}"] end end + + private + + def containers + Docker::Container + .all + .map { |dc| dc.json['Config']['Image'] } + .uniq + end + + def vulnerable_images + containers + .map { |container| Image.new(container, quayio_token, whitelist) } + .select(&:vulnerable?) + .map(&:name) + end end end end diff --git a/lib/quayio/scanner/image.rb b/lib/quayio/scanner/image.rb index f4fabfa..492182f 100644 --- a/lib/quayio/scanner/image.rb +++ b/lib/quayio/scanner/image.rb @@ -1,78 +1,46 @@ -require 'json' -require 'rest-client' - module Quayio module Scanner - class Image < Struct.new(:name, :quayio_token, :whitelist) - RELEVANT_SEVERITIES = %w(High Critical) - MAX_ATTEMPTS = 5 + class Image + RELEVANT_SEVERITIES = %w[High Critical].freeze + QUAY_IO_REPO_NAME = %r{quay.io\/(?[\w-]+)\/(?[\w-]+):(?[\w\.-]+)}.freeze + + attr_reader :name, :whitelist, :repository + + def initialize(name, quayio_token, whitelist) + @name = name + @whitelist = whitelist + + @name.match(QUAY_IO_REPO_NAME) do |r| + org, repo, tag = r.captures + @repository = Repository.new(quayio_token, org, repo, tag) + end + end def vulnerable? - quayio? && image_exists? && scanned? && high_vulnerabilities_present? + quayio? && scanned? && vulnerabilities_present? end private def quayio? - name.match(%r{^quay.io\/}) - end - - def image_exists? - raw_image + # safe guard, do not trust QUAY_IO_REPO_NAME regex match + !!name.match(%r{^quay.io\/}) end def scanned? raw_scan['status'] == 'scanned' end - def high_vulnerabilities_present? - raw_scan['data']['Layer']['Features'].detect do |f| - f['Vulnerabilities'] && f['Vulnerabilities'].detect do |v| - RELEVANT_SEVERITIES.include?(v['Severity']) && - !whitelist.include?(v['Name']) + def vulnerabilities_present? + !!raw_scan['data']['Layer']['Features'].detect do |f| + f['Vulnerabilities']&.detect do |v| + RELEVANT_SEVERITIES.include?(v['Severity']) && !whitelist.include?(v['Name']) end end end - def repo - name.split(':').first.gsub(%r{quay.io\/}, '') - end - - def tag - name.split(':').last - end - - def raw_image - return @raw_image if defined? @raw_image - - (1..MAX_ATTEMPTS).each do |attempt| - begin - response = RestClient.get( - "https://quay.io/api/v1/repository/#{repo}/tag/#{tag}/images", - authorization: "Bearer #{quayio_token}", - accept: :json) - rescue RestClient::ExceptionWithResponse => err - return nil if err.http_code == 404 # ignore unknown repos - if err.http_code == 520 and attempt < MAX_ATTEMPTS - sleep(rand(10)) - next - end - raise err - end - @raw_image = JSON.parse(response)['images'].first - return @raw_image - end - end - def raw_scan - return @raw_scan if defined? @raw_scan - - @raw_scan = begin - JSON.parse( - RestClient.get("https://quay.io/api/v1/repository/#{repo}/image/#{raw_image['id']}/security?vulnerabilities=true", - authorization: "Bearer #{quayio_token}", accept: :json) - ) - end + @raw_scan ||= repository.scan end end end diff --git a/lib/quayio/scanner/repository.rb b/lib/quayio/scanner/repository.rb new file mode 100644 index 0000000..70cc774 --- /dev/null +++ b/lib/quayio/scanner/repository.rb @@ -0,0 +1,42 @@ +require 'rest-client' +require 'json' + +module Quayio + module Scanner + Repository = Struct.new(:quayio_token, :org, :repo, :tag) do + MAX_ATTEMPTS = 5 + + def id + @id ||= fetch_id + end + + def scan + api_call("/image/#{id}/security?vulnerabilities=true") + end + + private + + def fetch_id + result = api_call("/tag/#{tag}/images") + (result['images'].first)['id'] + end + + def api_call(uri) + (1..Float::INFINITY).each do |attempt| + begin + response = RestClient.get( + "https://quay.io/api/v1/repository/#{org}/#{repo}#{uri}", + authorization: "Bearer #{quayio_token}", + accept: :json + ) + return JSON.parse(response) + rescue RestClient::ExceptionWithResponse => e + raise e if e.http_code != 520 || attempt >= MAX_ATTEMPTS + + sleep(rand(10)) + end + end + end + end + end +end diff --git a/quayio-scanner.gemspec b/quayio-scanner.gemspec index 9620bc1..c14be30 100644 --- a/quayio-scanner.gemspec +++ b/quayio-scanner.gemspec @@ -12,6 +12,8 @@ Gem::Specification.new do |spec| spec.homepage = 'https://github.com/aboutsource/quayio-scanner' spec.license = 'MIT' + spec.required_ruby_version = '>= 2.4.0' + spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end @@ -21,7 +23,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'docker-api', '~> 1.33' spec.add_dependency 'rest-client', '~> 2.0' spec.add_dependency 'sensu-plugin', '~> 2.1' - spec.add_development_dependency 'bundler', '~> 1.14' + spec.add_development_dependency 'bundler' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec', '~> 3.7' spec.add_development_dependency 'rubocop', '~> 0.49'