From 16e77a4e76f444708a8ceb2688e6d08c2a101b66 Mon Sep 17 00:00:00 2001 From: Benjamin Meichsner Date: Mon, 31 Jul 2017 12:45:37 +0200 Subject: [PATCH 1/5] Added sensu check and gem files. --- .gitignore | 10 ++++ Gemfile | 4 ++ LICENSE.txt | 21 +++++++++ Rakefile | 2 + bin/check-container-vulnerabilities.rb | 49 +++++++++++++++++++ bin/console | 14 ++++++ bin/setup | 8 ++++ lib/quayio/scanner.rb | 7 +++ lib/quayio/scanner/check.rb | 26 +++++++++++ lib/quayio/scanner/image.rb | 65 ++++++++++++++++++++++++++ lib/quayio/scanner/version.rb | 5 ++ quayio-scanner.gemspec | 29 ++++++++++++ 12 files changed, 240 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 LICENSE.txt create mode 100644 Rakefile create mode 100755 bin/check-container-vulnerabilities.rb create mode 100755 bin/console create mode 100755 bin/setup create mode 100644 lib/quayio/scanner.rb create mode 100644 lib/quayio/scanner/check.rb create mode 100644 lib/quayio/scanner/image.rb create mode 100644 lib/quayio/scanner/version.rb create mode 100644 quayio-scanner.gemspec 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..b1b9f70 --- /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 + result = Quayio::Scanner::Check.new(config[:docker_url], + config[:quayio_token]).run + + if result[0] == :ok + ok result[1] + else + critical result[1] + end + end +end diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..25d042b --- /dev/null +++ b/bin/console @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require 'bundler/setup' +require 'quayio/scanner' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require 'irb' +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here 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..afb3783 --- /dev/null +++ b/lib/quayio/scanner/image.rb @@ -0,0 +1,65 @@ +require 'rest-client' + +module Quayio + module Scanner + class Image < Struct.new(:name, :quayio_token) + 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| v['Severity'] == 'High' } + 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}/image", + authorization: "Bearer #{quayio_token}", accept: :json) + )['images'].detect { |i| i['tags'].include?(tag) } + 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 From ffbbf2def606877305d45cb571fba459840e4488 Mon Sep 17 00:00:00 2001 From: Benjamin Meichsner Date: Mon, 31 Jul 2017 13:35:02 +0200 Subject: [PATCH 2/5] Remove unneeded gem init stuff. --- bin/console | 14 -------------- bin/setup | 8 -------- 2 files changed, 22 deletions(-) delete mode 100755 bin/console delete mode 100755 bin/setup diff --git a/bin/console b/bin/console deleted file mode 100755 index 25d042b..0000000 --- a/bin/console +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env ruby - -require 'bundler/setup' -require 'quayio/scanner' - -# You can add fixtures and/or initialization code here to make experimenting -# with your gem easier. You can also use a different console, if you like. - -# (If you use this, don't forget to add pry to your Gemfile!) -# require "pry" -# Pry.start - -require 'irb' -IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup deleted file mode 100755 index dce67d8..0000000 --- a/bin/setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' -set -vx - -bundle install - -# Do any other automated setup that you need to do here From 33280714ebe12f7b957db3509b42cd37cc5208f7 Mon Sep 17 00:00:00 2001 From: Benjamin Meichsner Date: Mon, 31 Jul 2017 13:36:22 +0200 Subject: [PATCH 3/5] More descriptive check return. --- bin/check-container-vulnerabilities.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/check-container-vulnerabilities.rb b/bin/check-container-vulnerabilities.rb index b1b9f70..70e19aa 100755 --- a/bin/check-container-vulnerabilities.rb +++ b/bin/check-container-vulnerabilities.rb @@ -37,13 +37,13 @@ class CheckContainerVulnerabilities < Sensu::Plugin::Check::CLI long: '--quayio-token TOKEN' def run - result = Quayio::Scanner::Check.new(config[:docker_url], + status, message = Quayio::Scanner::Check.new(config[:docker_url], config[:quayio_token]).run - if result[0] == :ok - ok result[1] + if status == :ok + ok message else - critical result[1] + critical message end end end From cc7fb378c59798b8cd244fa8bc85c8f4d8cab0bb Mon Sep 17 00:00:00 2001 From: Benjamin Meichsner Date: Mon, 31 Jul 2017 13:42:36 +0200 Subject: [PATCH 4/5] Look for High and Critical severities. --- lib/quayio/scanner/image.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/quayio/scanner/image.rb b/lib/quayio/scanner/image.rb index afb3783..4a2d248 100644 --- a/lib/quayio/scanner/image.rb +++ b/lib/quayio/scanner/image.rb @@ -3,6 +3,8 @@ 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 @@ -24,7 +26,8 @@ def scanned? def high_vulnerabilities_present? raw_scan['data']['Layer']['Features'].detect do |f| f['Vulnerabilities'] && - f['Vulnerabilities'].detect { |v| v['Severity'] == 'High' } + f['Vulnerabilities'] + .detect { |v| RELEVANT_SEVERITIES.include?(v['Severity']) } end end From 98a2b36154420e3a1cbb15d3e5a87f520296bd24 Mon Sep 17 00:00:00 2001 From: Benjamin Meichsner Date: Mon, 31 Jul 2017 13:46:08 +0200 Subject: [PATCH 5/5] Simplified getting the image id from quay.io. --- lib/quayio/scanner/image.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/quayio/scanner/image.rb b/lib/quayio/scanner/image.rb index 4a2d248..dd5fc92 100644 --- a/lib/quayio/scanner/image.rb +++ b/lib/quayio/scanner/image.rb @@ -44,9 +44,9 @@ def raw_image @raw_image = begin JSON.parse( - RestClient.get("https://quay.io/api/v1/repository/#{repo}/image", + RestClient.get("https://quay.io/api/v1/repository/#{repo}/tag/#{tag}/images", authorization: "Bearer #{quayio_token}", accept: :json) - )['images'].detect { |i| i['tags'].include?(tag) } + )['images'].first rescue RestClient::ExceptionWithResponse => err return nil if err.http_code == 404 # ignore unknown repos raise err