Skip to content

Commit 167498a

Browse files
authored
Merge pull request #6 from aboutsource/fix/rate_limiting_on_scan
Fix rate limiting on scan
2 parents 5c6ac6a + 0dce8fc commit 167498a

7 files changed

Lines changed: 102 additions & 69 deletions

File tree

.rubocop.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Style/FrozenStringLiteralComment:
2+
Enabled: false
3+
4+
Style/Documentation:
5+
Enabled: false
6+
7+
Metrics/MethodLength:
8+
Max: 50
9+
10+
Metrics/BlockLength:
11+
Max: 200

bin/check-container-vulnerabilities.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ class CheckContainerVulnerabilities < Sensu::Plugin::Check::CLI
4444
proc: proc { |w| w.split(',') }
4545

4646
def run
47-
status, message = Quayio::Scanner::Check.new(
48-
config[:docker_url], config[:quayio_token], config[:whitelist]).run
47+
status, message = Quayio::Scanner::Check.new(config[:docker_url],
48+
config[:quayio_token],
49+
config[:whitelist]).run
4950

5051
if status == :ok
5152
ok message

lib/quayio/scanner.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
require 'quayio/scanner/version'
21
require 'quayio/scanner/check'
2+
require 'quayio/scanner/image'
3+
require 'quayio/scanner/repository'
4+
require 'quayio/scanner/version'
35

46
module Quayio
57
module Scanner

lib/quayio/scanner/check.rb

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
1-
require 'quayio/scanner/image'
21
require 'docker'
32

43
module Quayio
54
module Scanner
6-
class Check < Struct.new(:docker_url, :quayio_token, :whitelist)
5+
Check = Struct.new(:docker_url, :quayio_token, :whitelist) do
76
def run
87
Docker.url = docker_url
9-
containers = Docker::Container.all
10-
.map { |dc| dc.json['Config']['Image'] }
11-
.uniq
12-
13-
vulnerable_images = containers
14-
.map { |container| Image.new(container, quayio_token, whitelist) }
15-
.select(&:vulnerable?)
16-
.map(&:name)
178

189
if vulnerable_images.empty?
1910
[:ok, "#{containers.size} Containers are ok"]
2011
else
2112
[:critical, "The images are insecure: #{vulnerable_images.join(', ')}"]
2213
end
2314
end
15+
16+
private
17+
18+
def containers
19+
Docker::Container
20+
.all
21+
.map { |dc| dc.json['Config']['Image'] }
22+
.uniq
23+
end
24+
25+
def vulnerable_images
26+
containers
27+
.map { |container| Image.new(container, quayio_token, whitelist) }
28+
.select(&:vulnerable?)
29+
.map(&:name)
30+
end
2431
end
2532
end
2633
end

lib/quayio/scanner/image.rb

Lines changed: 23 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,46 @@
1-
require 'json'
2-
require 'rest-client'
3-
41
module Quayio
52
module Scanner
6-
class Image < Struct.new(:name, :quayio_token, :whitelist)
7-
RELEVANT_SEVERITIES = %w(High Critical)
8-
MAX_ATTEMPTS = 5
3+
class Image
4+
RELEVANT_SEVERITIES = %w[High Critical].freeze
5+
QUAY_IO_REPO_NAME = %r{quay.io\/(?<org>[\w-]+)\/(?<repo>[\w-]+):(?<tag>[\w\.-]+)}.freeze
6+
7+
attr_reader :name, :whitelist, :repository
8+
9+
def initialize(name, quayio_token, whitelist)
10+
@name = name
11+
@whitelist = whitelist
12+
13+
@name.match(QUAY_IO_REPO_NAME) do |r|
14+
org, repo, tag = r.captures
15+
@repository = Repository.new(quayio_token, org, repo, tag)
16+
end
17+
end
918

1019
def vulnerable?
11-
quayio? && image_exists? && scanned? && high_vulnerabilities_present?
20+
quayio? && scanned? && vulnerabilities_present?
1221
end
1322

1423
private
1524

1625
def quayio?
17-
name.match(%r{^quay.io\/})
18-
end
19-
20-
def image_exists?
21-
raw_image
26+
# safe guard, do not trust QUAY_IO_REPO_NAME regex match
27+
!!name.match(%r{^quay.io\/})
2228
end
2329

2430
def scanned?
2531
raw_scan['status'] == 'scanned'
2632
end
2733

28-
def high_vulnerabilities_present?
29-
raw_scan['data']['Layer']['Features'].detect do |f|
30-
f['Vulnerabilities'] && f['Vulnerabilities'].detect do |v|
31-
RELEVANT_SEVERITIES.include?(v['Severity']) &&
32-
!whitelist.include?(v['Name'])
34+
def vulnerabilities_present?
35+
!!raw_scan['data']['Layer']['Features'].detect do |f|
36+
f['Vulnerabilities']&.detect do |v|
37+
RELEVANT_SEVERITIES.include?(v['Severity']) && !whitelist.include?(v['Name'])
3338
end
3439
end
3540
end
3641

37-
def repo
38-
name.split(':').first.gsub(%r{quay.io\/}, '')
39-
end
40-
41-
def tag
42-
name.split(':').last
43-
end
44-
45-
def raw_image
46-
return @raw_image if defined? @raw_image
47-
48-
(1..MAX_ATTEMPTS).each do |attempt|
49-
begin
50-
response = RestClient.get(
51-
"https://quay.io/api/v1/repository/#{repo}/tag/#{tag}/images",
52-
authorization: "Bearer #{quayio_token}",
53-
accept: :json)
54-
rescue RestClient::ExceptionWithResponse => err
55-
return nil if err.http_code == 404 # ignore unknown repos
56-
if err.http_code == 520 and attempt < MAX_ATTEMPTS
57-
sleep(rand(10))
58-
next
59-
end
60-
raise err
61-
end
62-
@raw_image = JSON.parse(response)['images'].first
63-
return @raw_image
64-
end
65-
end
66-
6742
def raw_scan
68-
return @raw_scan if defined? @raw_scan
69-
70-
@raw_scan = begin
71-
JSON.parse(
72-
RestClient.get("https://quay.io/api/v1/repository/#{repo}/image/#{raw_image['id']}/security?vulnerabilities=true",
73-
authorization: "Bearer #{quayio_token}", accept: :json)
74-
)
75-
end
43+
@raw_scan ||= repository.scan
7644
end
7745
end
7846
end

lib/quayio/scanner/repository.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'rest-client'
2+
require 'json'
3+
4+
module Quayio
5+
module Scanner
6+
Repository = Struct.new(:quayio_token, :org, :repo, :tag) do
7+
MAX_ATTEMPTS = 5
8+
9+
def id
10+
@id ||= fetch_id
11+
end
12+
13+
def scan
14+
api_call("/image/#{id}/security?vulnerabilities=true")
15+
end
16+
17+
private
18+
19+
def fetch_id
20+
result = api_call("/tag/#{tag}/images")
21+
(result['images'].first)['id']
22+
end
23+
24+
def api_call(uri)
25+
(1..Float::INFINITY).each do |attempt|
26+
begin
27+
response = RestClient.get(
28+
"https://quay.io/api/v1/repository/#{org}/#{repo}#{uri}",
29+
authorization: "Bearer #{quayio_token}",
30+
accept: :json
31+
)
32+
return JSON.parse(response)
33+
rescue RestClient::ExceptionWithResponse => e
34+
raise e if e.http_code != 520 || attempt >= MAX_ATTEMPTS
35+
36+
sleep(rand(10))
37+
end
38+
end
39+
end
40+
end
41+
end
42+
end

quayio-scanner.gemspec

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Gem::Specification.new do |spec|
1212
spec.homepage = 'https://github.com/aboutsource/quayio-scanner'
1313
spec.license = 'MIT'
1414

15+
spec.required_ruby_version = '>= 2.4.0'
16+
1517
spec.files = `git ls-files -z`.split("\x0").reject do |f|
1618
f.match(%r{^(test|spec|features)/})
1719
end
@@ -21,7 +23,7 @@ Gem::Specification.new do |spec|
2123
spec.add_dependency 'docker-api', '~> 1.33'
2224
spec.add_dependency 'rest-client', '~> 2.0'
2325
spec.add_dependency 'sensu-plugin', '~> 2.1'
24-
spec.add_development_dependency 'bundler', '~> 1.14'
26+
spec.add_development_dependency 'bundler'
2527
spec.add_development_dependency 'rake', '~> 10.0'
2628
spec.add_development_dependency 'rspec', '~> 3.7'
2729
spec.add_development_dependency 'rubocop', '~> 0.49'

0 commit comments

Comments
 (0)