diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64f7e02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +/vendor/bundle diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..d8b5723 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in quayio-scanner.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..5d8ef64 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Benjamin Meichsner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..d65c578 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +require 'bundler/gem_tasks' +task default: :spec diff --git a/bin/check-container-vulnerabilities.rb b/bin/check-container-vulnerabilities.rb new file mode 100755 index 0000000..70e19aa --- /dev/null +++ b/bin/check-container-vulnerabilities.rb @@ -0,0 +1,49 @@ +#! /usr/bin/env ruby +# +# check-container-vulnerabilities +# +# DESCRIPTION: +# +# This plugin attempts to fetch vulnerabilties for all running containers +# +# OUTPUT: +# plain text +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: docker-api +# gem: rest-client +# +# USAGE: +# ./check-container-vulnerabilities.rb -d -t +# + +require 'sensu-plugin/check/cli' +require 'quayio/scanner' + +class CheckContainerVulnerabilities < Sensu::Plugin::Check::CLI + option :docker_url, + description: 'Docker URL', + short: '-d URL', + long: '--docker-url URL', + default: 'unix:///var/run/docker.sock' + + option :quayio_token, + description: 'Quay.io oauth token', + short: '-t TOKEN', + long: '--quayio-token TOKEN' + + def run + status, message = Quayio::Scanner::Check.new(config[:docker_url], + config[:quayio_token]).run + + if status == :ok + ok message + else + critical message + end + end +end diff --git a/lib/quayio/scanner.rb b/lib/quayio/scanner.rb new file mode 100644 index 0000000..f93f57d --- /dev/null +++ b/lib/quayio/scanner.rb @@ -0,0 +1,7 @@ +require 'quayio/scanner/version' +require 'quayio/scanner/check' + +module Quayio + module Scanner + end +end diff --git a/lib/quayio/scanner/check.rb b/lib/quayio/scanner/check.rb new file mode 100644 index 0000000..cbe3c8d --- /dev/null +++ b/lib/quayio/scanner/check.rb @@ -0,0 +1,26 @@ +require 'quayio/scanner/image' +require 'docker' + +module Quayio + module Scanner + class Check < Struct.new(:docker_url, :quayio_token) + 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) } + .select(&:vulnerable?) + .map(&:name) + + if vulnerable_images.empty? + [:ok, "#{containers.size} Containers are ok"] + else + [:critical, "The images are insecure: #{vulnerable_images.join(', ')}"] + end + end + end + end +end diff --git a/lib/quayio/scanner/image.rb b/lib/quayio/scanner/image.rb new file mode 100644 index 0000000..dd5fc92 --- /dev/null +++ b/lib/quayio/scanner/image.rb @@ -0,0 +1,68 @@ +require 'rest-client' + +module Quayio + module Scanner + class Image < Struct.new(:name, :quayio_token) + RELEVANT_SEVERITIES = %w(High Critical) + + def vulnerable? + quayio? && image_exists? && scanned? && high_vulnerabilities_present? + end + + private + + def quayio? + name.match(%r{^quay.io\/}) + end + + def image_exists? + raw_image + 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 { |v| RELEVANT_SEVERITIES.include?(v['Severity']) } + 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 + + @raw_image = begin + JSON.parse( + RestClient.get("https://quay.io/api/v1/repository/#{repo}/tag/#{tag}/images", + authorization: "Bearer #{quayio_token}", accept: :json) + )['images'].first + rescue RestClient::ExceptionWithResponse => err + return nil if err.http_code == 404 # ignore unknown repos + raise err + 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 + end + end + end +end diff --git a/lib/quayio/scanner/version.rb b/lib/quayio/scanner/version.rb new file mode 100644 index 0000000..8ae342c --- /dev/null +++ b/lib/quayio/scanner/version.rb @@ -0,0 +1,5 @@ +module Quayio + module Scanner + VERSION = '0.1.0'.freeze + end +end diff --git a/quayio-scanner.gemspec b/quayio-scanner.gemspec new file mode 100644 index 0000000..549ee33 --- /dev/null +++ b/quayio-scanner.gemspec @@ -0,0 +1,29 @@ +# coding: utf-8 + +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'quayio/scanner/version' + +Gem::Specification.new do |spec| + spec.name = 'quayio-scanner' + spec.version = Quayio::Scanner::VERSION + spec.authors = ['Benjamin Meichsner'] + spec.email = ['benjamin.meichsner@aboutsource.net'] + + spec.summary = 'Scan quay.io for vulnerabilties in running containers.' + spec.homepage = 'https://github.com/aboutsource/quayio-scanner' + spec.license = 'MIT' + + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end + spec.executables = Dir.glob('bin/**/*.rb').map { |file| File.basename(file) } + spec.require_paths = ['lib'] + + spec.add_dependency 'sensu-plugin' + spec.add_dependency 'docker-api' + spec.add_dependency 'rest-client' + spec.add_development_dependency 'bundler', '~> 1.14' + spec.add_development_dependency 'rake', '~> 10.0' + spec.add_development_dependency 'rubocop' +end