dobby 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +16 -0
- data/.rubocop.yml +30 -0
- data/.rubocop_todo.yml +42 -0
- data/.travis.yml +12 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +60 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +103 -0
- data/Rakefile +8 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/config/default.yml +8 -0
- data/dobby.gemspec +58 -0
- data/lib/dobby.rb +51 -0
- data/lib/dobby/builtins.rb +17 -0
- data/lib/dobby/cli.rb +64 -0
- data/lib/dobby/configuration.rb +58 -0
- data/lib/dobby/database.rb +62 -0
- data/lib/dobby/defect.rb +74 -0
- data/lib/dobby/dpkg.rb +21 -0
- data/lib/dobby/error.rb +6 -0
- data/lib/dobby/flag_manager.rb +67 -0
- data/lib/dobby/flags.yml +8 -0
- data/lib/dobby/formatter/abstract_formatter.rb +25 -0
- data/lib/dobby/formatter/colorizable.rb +41 -0
- data/lib/dobby/formatter/formatter_set.rb +79 -0
- data/lib/dobby/formatter/json_formatter.rb +42 -0
- data/lib/dobby/formatter/simple_formatter.rb +54 -0
- data/lib/dobby/options.rb +149 -0
- data/lib/dobby/package.rb +156 -0
- data/lib/dobby/package_source/abstract_package_source.rb +17 -0
- data/lib/dobby/package_source/dpkg_status_file.rb +85 -0
- data/lib/dobby/runner.rb +152 -0
- data/lib/dobby/scanner.rb +128 -0
- data/lib/dobby/severity.rb +66 -0
- data/lib/dobby/strategy.rb +168 -0
- data/lib/dobby/update_response.rb +19 -0
- data/lib/dobby/version.rb +24 -0
- data/lib/dobby/vuln_source/abstract_vuln_source.rb +26 -0
- data/lib/dobby/vuln_source/debian.rb +166 -0
- data/lib/dobby/vuln_source/ubuntu.rb +229 -0
- metadata +45 -1
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
# The Strategy provides base functionality for defining how Dobby takes in
|
5
|
+
# the data it needs to do its job. Each strategy should include this mixin
|
6
|
+
# to ensure compatibility with the rest of the library.
|
7
|
+
#
|
8
|
+
# In particular, the mixin provides a DSL for configuring strategies and
|
9
|
+
# standardizes some strategy behavior (such as #inspect).
|
10
|
+
#
|
11
|
+
# Much of this borrowed from Omniauth::Strategy
|
12
|
+
# https://github.com/omniauth/omniauth/blob/master/lib/omniauth/strategy.rb
|
13
|
+
#
|
14
|
+
# @abstract Include as a mixin to implement a Strategy compatible with the
|
15
|
+
# library
|
16
|
+
module Strategy
|
17
|
+
class Options < Hashie::Mash; end
|
18
|
+
|
19
|
+
def self.included(base)
|
20
|
+
Dobby.strategies << base
|
21
|
+
|
22
|
+
base.extend ClassMethods
|
23
|
+
base.class_eval do
|
24
|
+
option :setup, false
|
25
|
+
option :test_mode, false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Extensible configuration for implementers.
|
30
|
+
module ClassMethods
|
31
|
+
# An inherited set of default options set at the class-level
|
32
|
+
# for each strategy.
|
33
|
+
#
|
34
|
+
# @return [Options]
|
35
|
+
def default_options
|
36
|
+
existing = begin
|
37
|
+
superclass.default_options
|
38
|
+
rescue StandardError
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
@default_options ||= Dobby::Strategy::Options.new(existing)
|
42
|
+
end
|
43
|
+
|
44
|
+
# This allows for more declarative subclassing of strategies by allowing
|
45
|
+
# default options to be set using a simple configure call.
|
46
|
+
#
|
47
|
+
# @param options [Hash] If supplied, these will be the default options
|
48
|
+
# (deep-merged into the superclass's default options).
|
49
|
+
# @yield [Options] The options Mash that allows you to set your defaults
|
50
|
+
# as you'd like.
|
51
|
+
#
|
52
|
+
# @example Using a yield to configure the default options.
|
53
|
+
#
|
54
|
+
# class MyStrategy
|
55
|
+
# include Dobby::Strategy
|
56
|
+
#
|
57
|
+
# configure do |c|
|
58
|
+
# c.foo = 'bar'
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @example Using a hash to configure the default options.
|
63
|
+
#
|
64
|
+
# class MyStrategy
|
65
|
+
# include Dobby::Strategy
|
66
|
+
# configure foo: 'bar'
|
67
|
+
def configure(options = nil)
|
68
|
+
if block_given?
|
69
|
+
yield default_options
|
70
|
+
else
|
71
|
+
default_options.deep_merge!(options)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Directly declare a default option for your class. This is a useful from
|
76
|
+
# a documentation perspective as it provides a simple line-by-line analysis
|
77
|
+
# of the kinds of options your strategy provides by default.
|
78
|
+
#
|
79
|
+
# @param name [Symbol] The key of the default option in your configuration hash.
|
80
|
+
# @param value [Object] The value your object defaults to. Nil if not provided.
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
#
|
84
|
+
# class MyStrategy
|
85
|
+
# include Dobby::Strategy
|
86
|
+
#
|
87
|
+
# option :foo, 'bar'
|
88
|
+
# end
|
89
|
+
def option(name, value = nil)
|
90
|
+
default_options[name] = value
|
91
|
+
end
|
92
|
+
|
93
|
+
# Sets (and retrieves) option key names for initializer arguments to be
|
94
|
+
# recorded as. This takes care of 90% of the use cases for overriding
|
95
|
+
# the initializer in Dobby Strategies. Dobby::Options will also use
|
96
|
+
# this, via #cli_options, to configure any command line options.
|
97
|
+
def args(args = nil)
|
98
|
+
if args
|
99
|
+
@args = Array(args)
|
100
|
+
return
|
101
|
+
end
|
102
|
+
existing = begin
|
103
|
+
superclass.args
|
104
|
+
rescue StandardError
|
105
|
+
[]
|
106
|
+
end
|
107
|
+
(instance_variable_defined?(:@args) && @args) || existing
|
108
|
+
end
|
109
|
+
|
110
|
+
# By default, all args are automatically built out as k/v CLI options. For
|
111
|
+
# more advanced behavior, override cli_options in the implementing class.
|
112
|
+
# The return of this method is passed directly to Dobby::Options#options
|
113
|
+
def cli_options
|
114
|
+
args.map { |arg| "--#{arg.to_s.tr('_', '-')} VALUE" }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
attr_reader :options
|
119
|
+
|
120
|
+
# Initialize the strategy, creating an [Options] hash if the last
|
121
|
+
# argument is a Hash.
|
122
|
+
#
|
123
|
+
# @param args [Hash]
|
124
|
+
#
|
125
|
+
# @yield [Options]
|
126
|
+
def initialize(*args)
|
127
|
+
@options = self.class.default_options.dup
|
128
|
+
options.deep_merge!(args.pop) if args.last.is_a?(Hash)
|
129
|
+
options[:name] ||= self.class.to_s.split('::').last.downcase
|
130
|
+
|
131
|
+
self.class.args.each do |arg|
|
132
|
+
break if args.empty?
|
133
|
+
|
134
|
+
options[arg] = args.shift
|
135
|
+
end
|
136
|
+
|
137
|
+
raise ArgumentError, "Received too many arguments. #{args.inspect}" unless args.empty?
|
138
|
+
|
139
|
+
setup
|
140
|
+
|
141
|
+
yield options if block_given?
|
142
|
+
end
|
143
|
+
|
144
|
+
# Callback placeholder so that 'super' during initialize is unnecessary in
|
145
|
+
# implementing classes. This is the other 10% of the use case for overriding
|
146
|
+
# initialize.
|
147
|
+
def setup; end
|
148
|
+
|
149
|
+
# @return [String]
|
150
|
+
def inspect
|
151
|
+
"#<#{self.class}>"
|
152
|
+
end
|
153
|
+
|
154
|
+
# Access to the Dobby logger, automatically prefixed with the
|
155
|
+
# strategy's name.
|
156
|
+
#
|
157
|
+
# @param level [Symbol] syslog level
|
158
|
+
# @param message [String]
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# log :fatal, 'This is a fatal error.'
|
162
|
+
# log :error, 'This is an error.'
|
163
|
+
# log :warn, 'This is a warning.'
|
164
|
+
def log(level, message)
|
165
|
+
Dobby.logger.send(level, "(#{self.class.name}) #{message}")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
# A generic response format.
|
5
|
+
class UpdateResponse
|
6
|
+
attr_reader :content
|
7
|
+
# @param changed [Boolean]
|
8
|
+
# @param content [Hash]
|
9
|
+
def initialize(changed, content = nil)
|
10
|
+
@changed = changed
|
11
|
+
@content = content
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Boolean]
|
15
|
+
def changed?
|
16
|
+
@changed == true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
# Dobby version information.
|
5
|
+
module Version
|
6
|
+
STRING = '0.1.2'
|
7
|
+
MSG = '%<version>s (AptPkg %<aptpkg_version>s Apt %<apt_version>s '\
|
8
|
+
'libapt %<libapt_version>s) running on %<linux_version>s '\
|
9
|
+
'%<ruby_engine>s %<ruby_version>s %<ruby_platform>s'
|
10
|
+
|
11
|
+
def self.version(debug = false)
|
12
|
+
if debug
|
13
|
+
format(MSG, version: STRING, aptpkg_version: Debian::AptPkg::VERSION,
|
14
|
+
apt_version: Debian::AptPkg::APT_VERSION,
|
15
|
+
libapt_version: Debian::AptPkg::LIBAPT_PKG_VERSION,
|
16
|
+
linux_version: Etc.uname[:version],
|
17
|
+
ruby_engine: RUBY_ENGINE, ruby_version: RUBY_VERSION,
|
18
|
+
ruby_platform: RUBY_PLATFORM)
|
19
|
+
else
|
20
|
+
STRING
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
module VulnSource
|
5
|
+
# @abstract Subclass and override {#update} and #{clean} to implement a
|
6
|
+
# custom Database source.
|
7
|
+
class AbstractVulnSource
|
8
|
+
include Dobby::Strategy
|
9
|
+
|
10
|
+
# Retrieve a source database (if necessary) and parse it.
|
11
|
+
#
|
12
|
+
# @return [UpdateResponse]
|
13
|
+
def update
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
# Instruct a strategy to clean up after itself, removing any files it
|
18
|
+
# may have created.
|
19
|
+
#
|
20
|
+
# @return [void]
|
21
|
+
def clean
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
module VulnSource
|
5
|
+
# Vulnerability database source for Debian systems. This uses the JSON file
|
6
|
+
# provided by the Debian Security Tracker as its' remote source.
|
7
|
+
class Debian < AbstractVulnSource
|
8
|
+
DEFAULT_RELEASE = 'jessie'
|
9
|
+
|
10
|
+
args %i[releases cve_url_prefix dst_json_uri dst_local_file]
|
11
|
+
|
12
|
+
option :test_mode, false
|
13
|
+
option :releases, [DEFAULT_RELEASE]
|
14
|
+
|
15
|
+
option :dst_json_uri, 'https://security-tracker.debian.org/tracker/data/json'
|
16
|
+
option :cve_url_prefix, 'https://security-tracker.debian.org/tracker/'
|
17
|
+
|
18
|
+
# rubocop:disable Layout/AlignArray
|
19
|
+
def self.cli_options
|
20
|
+
[
|
21
|
+
['--releases ONE,TWO', 'Limit the packages returned by a VulnSource to',
|
22
|
+
'these releases. Default varies with selected',
|
23
|
+
'VulnSource.'],
|
24
|
+
['--dst-json-uri URI', 'VulnSource::Debian -- specify a URI to the',
|
25
|
+
"Debian Security Tracker's JSON file."],
|
26
|
+
['--dst-local-file PATH', 'VulnSource::Debian -- If provided, read from',
|
27
|
+
'the specified file instead of requesting the',
|
28
|
+
'DST json file from a remote.'],
|
29
|
+
['--cve-url-prefix URI', 'URI prefix used for building CVE links.']
|
30
|
+
]
|
31
|
+
end
|
32
|
+
# rubocop:enable Layout/AlignArray
|
33
|
+
|
34
|
+
# Map of DST-provided urgencies to a common severity format
|
35
|
+
URGENCY_MAP = Hash.new(Severity::Unknown).merge(
|
36
|
+
'not-yet-assigned' => Severity::Unknown,
|
37
|
+
'end-of-life' => Severity::Negligible,
|
38
|
+
'unimportant' => Severity::Negligible,
|
39
|
+
'low' => Severity::Low,
|
40
|
+
'low*' => Severity::Low,
|
41
|
+
'low**' => Severity::Low,
|
42
|
+
'medium' => Severity::Medium,
|
43
|
+
'medium*' => Severity::Medium,
|
44
|
+
'medium**' => Severity::Medium,
|
45
|
+
'high' => Severity::High,
|
46
|
+
'high*' => Severity::High,
|
47
|
+
'high**' => Severity::Critical
|
48
|
+
)
|
49
|
+
|
50
|
+
# Unable to retrieve or load a dobby database
|
51
|
+
class NoDataError < Error; end
|
52
|
+
|
53
|
+
# Received a non-200 response from the Security Tracker
|
54
|
+
class BadResponseError < Error
|
55
|
+
attr_accessor :curl
|
56
|
+
|
57
|
+
def initialize(curl)
|
58
|
+
url = curl.url
|
59
|
+
url_path_only = url =~ /\A([^?]*)\?/ ? Regexp.last_match(1) : url
|
60
|
+
super("Bad response code (#{curl.response_code.to_i} for #{url_path_only})")
|
61
|
+
@curl = curl
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Hash{resp_code=>Integer, url=>String, resp=>String}]
|
65
|
+
def context
|
66
|
+
{ resp_code: @curl.response_code, url: @curl.url, resp: @curl.body_str }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
###
|
70
|
+
|
71
|
+
# Initialize callback.
|
72
|
+
def setup
|
73
|
+
@last_hash = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# Provide an UpdateResponse sourced from the Debian Security Tracker's
|
77
|
+
# JSON. If the SHA256 of the returned JSON matches the last attempt,
|
78
|
+
# UpdateResponse.changed? will be false. Otherwise, UpdateResponse.content
|
79
|
+
# will be a Hash{package_name=>Array<Defect>}
|
80
|
+
#
|
81
|
+
# @return [UpdateResponse]
|
82
|
+
def update
|
83
|
+
data = fetch_from_remote(options.dst_json_uri)
|
84
|
+
|
85
|
+
hash = Digest::SHA256.hexdigest(data)
|
86
|
+
return UpdateResponse.new(false) if hash == @last_hash
|
87
|
+
|
88
|
+
debian_vulns = Oj.load(data)
|
89
|
+
|
90
|
+
vuln_entries = Hash.new { |h, k| h[k] = [] }
|
91
|
+
debian_vulns.each do |package, vulns|
|
92
|
+
vulns.each do |identifier, vuln|
|
93
|
+
# If a permanent ID has not been assigned to the vuln, skip it
|
94
|
+
next unless identifier.start_with?('CVE-', 'OVE-')
|
95
|
+
|
96
|
+
severity = Severity::Unknown
|
97
|
+
fixed_versions = []
|
98
|
+
|
99
|
+
vuln['releases'].each do |release, info|
|
100
|
+
next unless options.releases.include?(release)
|
101
|
+
|
102
|
+
version = choose_version(info['fixed_version'], info['status'])
|
103
|
+
next unless version
|
104
|
+
|
105
|
+
# For a given Defect, it may have differing severities across
|
106
|
+
# different Debian releases. Set the severity of the Defect to
|
107
|
+
# the highest value.
|
108
|
+
new_severity = URGENCY_MAP[info['urgency']]
|
109
|
+
severity = new_severity if severity < new_severity
|
110
|
+
|
111
|
+
fixed_versions << Package.new(
|
112
|
+
name: package,
|
113
|
+
version: version,
|
114
|
+
release: release
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
vuln_entries[package] << Defect.new(
|
119
|
+
identifier: identifier,
|
120
|
+
description: vuln['description'],
|
121
|
+
severity: severity,
|
122
|
+
fixed_in: fixed_versions,
|
123
|
+
link: options.cve_url_prefix + identifier
|
124
|
+
)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
@last_hash = hash
|
128
|
+
UpdateResponse.new(true, vuln_entries)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Given a 'fixed in' version and a defect status, determine what version
|
132
|
+
# represents the status for comparison.
|
133
|
+
#
|
134
|
+
# @param fixed [String] version that the vuln src says a defect is fixed in
|
135
|
+
# @param status [String] the current status of the defect
|
136
|
+
#
|
137
|
+
# @return [String] version string
|
138
|
+
def choose_version(fixed, status)
|
139
|
+
return unless status
|
140
|
+
return Package::MIN_VERSION if fixed == '0'
|
141
|
+
return Package::MAX_VERSION if status == 'open'
|
142
|
+
return fixed if status == 'resolved'
|
143
|
+
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
|
147
|
+
# Retrieve the DST json file
|
148
|
+
#
|
149
|
+
# @param url [String]
|
150
|
+
#
|
151
|
+
# @return [String]
|
152
|
+
#
|
153
|
+
# @raise [BadResponseError] if url returns something other than 200
|
154
|
+
# @raise [NoDataError] if url returns no data
|
155
|
+
def fetch_from_remote(url)
|
156
|
+
return File.read(options.dst_local_file) if options.dst_local_file
|
157
|
+
|
158
|
+
curl = Curl::Easy.perform(url)
|
159
|
+
raise BadResponseError, curl unless curl.response_code.to_i == 200
|
160
|
+
raise NoDataError unless curl.body_str
|
161
|
+
|
162
|
+
curl.body_str
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
module VulnSource
|
5
|
+
# Vulnerability source for Ubuntu systems. This class uses the Ubuntu CVE
|
6
|
+
# Tracker as its' remote source by checking out the bazaar repository.
|
7
|
+
#
|
8
|
+
# @note This requires bazaar to be installed at /usr/bin/bzar unless
|
9
|
+
# configured with a different path via the bzr option.
|
10
|
+
class Ubuntu < AbstractVulnSource
|
11
|
+
DEFAULT_RELEASE = 'xenial'
|
12
|
+
args %i[releases cve_url_prefix bzr_bin bzr_repo tracker_repo]
|
13
|
+
|
14
|
+
option :test_mode, false
|
15
|
+
option :releases, [DEFAULT_RELEASE]
|
16
|
+
|
17
|
+
option :bzr_bin, '/usr/bin/bzr'
|
18
|
+
option :cve_url_prefix, 'http://people.ubuntu.com/~ubuntu-security/cve/'
|
19
|
+
option :tracker_repo, 'https://launchpad.net/ubuntu-cve-tracker'
|
20
|
+
|
21
|
+
# rubocop:disable Layout/AlignArray
|
22
|
+
def self.cli_options
|
23
|
+
[
|
24
|
+
['--releases ONE,TWO', 'Limit the packages returned by a VulnSource to',
|
25
|
+
'these releases. Default vaires with selected',
|
26
|
+
'VulnSource.'],
|
27
|
+
['--bzr-bin PATH', 'VulnSource::Ubuntu - Path to the "bzr" binary.'],
|
28
|
+
# ['--bzr-repo PATH', 'Path to the Ubuntu Security bazaar repo on the',
|
29
|
+
# 'local system.'],
|
30
|
+
['--tracker-repo URI', 'VulnSource::Ubuntu - Path to the security tracker',
|
31
|
+
'bazaar repository remote.'],
|
32
|
+
['--cve-url-prefix URL', 'URI prefix used for building CVE links.']
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
# rubocop:enable Layout/AlignArray
|
37
|
+
# Map of Canonical-provided urgencies to a common severity format
|
38
|
+
URGENCY_MAP = Hash.new(Severity::Unknown).merge(
|
39
|
+
'untriaged' => Severity::Unknown,
|
40
|
+
'negligible' => Severity::Negligible,
|
41
|
+
'low' => Severity::Low,
|
42
|
+
'medium' => Severity::Medium,
|
43
|
+
'high' => Severity::High,
|
44
|
+
'critical' => Severity::Critical
|
45
|
+
)
|
46
|
+
|
47
|
+
# An array of defect states that we are interested in. Skips e.g. ignored/DNE
|
48
|
+
RELEVANT_STATUSES = %w[needed active deferred not-affected released].freeze
|
49
|
+
|
50
|
+
# Line prefixes which indicate the end of a defect description
|
51
|
+
DESC_STOP_FIELDS = %w[
|
52
|
+
Ubuntu-Description:
|
53
|
+
Priority:
|
54
|
+
Discovered-By:
|
55
|
+
Notes:
|
56
|
+
Bugs:
|
57
|
+
Assigned-to:
|
58
|
+
].freeze
|
59
|
+
|
60
|
+
# A hash with DeepMerge
|
61
|
+
class VulnerabilityHash < Hash
|
62
|
+
include Hashie::Extensions::DeepMerge
|
63
|
+
end
|
64
|
+
|
65
|
+
def setup
|
66
|
+
@last_revno = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# Provide an UpdateReponse sourced from Canoncial's Ubuntu CVE Tracker
|
70
|
+
# repository. This is a bazaar repository, and thus this strategy depends
|
71
|
+
# on the bzr binary being available. The strategy will avoid descending
|
72
|
+
# the repository if the repo's revno matches a previous revno.
|
73
|
+
#
|
74
|
+
# @return [UpdateResponse]
|
75
|
+
def update
|
76
|
+
branch_or_pull
|
77
|
+
revno = bzr_revno
|
78
|
+
return UpdateResponse.new(false) if revno == @last_revno
|
79
|
+
|
80
|
+
vuln_entries = VulnerabilityHash.new
|
81
|
+
modified_entries.each do |file|
|
82
|
+
data = parse_ubuntu_cve_file(File.readlines(file))
|
83
|
+
vuln_entries.deep_merge!(data)
|
84
|
+
end
|
85
|
+
@last_revno = revno
|
86
|
+
UpdateResponse.new(true, vuln_entries)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Delete the bzr repository
|
90
|
+
def clean
|
91
|
+
Dir.rmdir(options.local_repo_path)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Determine whether the repo needs to be branched (because it doesn't
|
97
|
+
# exist) or pulled (because it already exists), and then do that.
|
98
|
+
#
|
99
|
+
# @return [Boolean]
|
100
|
+
def branch_or_pull
|
101
|
+
if Dir.exist?(options.local_repo_path)
|
102
|
+
pull(options.local_repo_path)
|
103
|
+
else
|
104
|
+
branch(options.local_repo_path)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def branch(path)
|
109
|
+
FileUtils.mkdir_p path
|
110
|
+
Dir.chdir(path) do
|
111
|
+
return system(options.bzr_bin.to_s, 'branch', '--use-existing-dir',
|
112
|
+
options.tracker_repo.to_s, '.')
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def pull(path)
|
117
|
+
Dir.chdir(path) do
|
118
|
+
return system(options.bzr_bin.to_s, 'pull', '--overwrite')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Retrieve bazaar revision number
|
123
|
+
#
|
124
|
+
# @return [String]
|
125
|
+
def bzr_revno
|
126
|
+
stdout, = Open3.capture2(options.bzr_bin, 'revno', options.local_repo_path)
|
127
|
+
stdout.strip
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns a list of all interesting files in the bazaar repository.
|
131
|
+
#
|
132
|
+
# @note The library cannot currently support differential updates :(
|
133
|
+
#
|
134
|
+
# @return [Array<String>]
|
135
|
+
def modified_entries
|
136
|
+
search = File.join(options.local_repo_path, '{active,retired}', '**', 'CVE*')
|
137
|
+
Dir.glob(search)
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_ubuntu_cve_file(file_lines)
|
141
|
+
entries = Hash.new { |h, k| h[k] = {} }
|
142
|
+
fixed_versions = Hash.new { |h, k| h[k] = [] }
|
143
|
+
severity = Severity::Unknown
|
144
|
+
|
145
|
+
identifier = description = link = nil
|
146
|
+
more = false
|
147
|
+
|
148
|
+
file_lines.each do |line|
|
149
|
+
line.chomp!
|
150
|
+
next if line.start_with?('#') || line.empty?
|
151
|
+
|
152
|
+
if line.start_with?('Candidate:')
|
153
|
+
identifier = line.split[1]
|
154
|
+
link = options.cve_url_prefix + identifier
|
155
|
+
next
|
156
|
+
elsif line.start_with?('Priority:')
|
157
|
+
severity = URGENCY_MAP[line.split[1]]
|
158
|
+
next
|
159
|
+
elsif line.start_with?('Description:')
|
160
|
+
more = true
|
161
|
+
check = line.split(' ', 2)
|
162
|
+
description = check[1] if check.count > 1
|
163
|
+
next
|
164
|
+
elsif more
|
165
|
+
if line.start_with?(*DESC_STOP_FIELDS)
|
166
|
+
description.strip!
|
167
|
+
more = false
|
168
|
+
else
|
169
|
+
description = description + ' ' + line.strip
|
170
|
+
end
|
171
|
+
next
|
172
|
+
end
|
173
|
+
|
174
|
+
# Separate release, package status and version information out of a
|
175
|
+
# defect detail line.
|
176
|
+
#
|
177
|
+
# Example line -
|
178
|
+
# xenial_linux: released (4.4.0-81.104)
|
179
|
+
|
180
|
+
# rubocop:disable Metrics/LineLength
|
181
|
+
next unless /(?<release>.*)_(?<package>.*): (?<status>[^\s]*)( \(+(?<note>[^()]*)\)+)?/ =~ line
|
182
|
+
# rubocop:enable Metrics/LineLength
|
183
|
+
|
184
|
+
next unless RELEVANT_STATUSES.include?(status)
|
185
|
+
|
186
|
+
release = release.split('/')[0]
|
187
|
+
next unless options.releases.include?(release)
|
188
|
+
|
189
|
+
version = choose_version(note, status)
|
190
|
+
fixed_versions[package] << Package.new(
|
191
|
+
name: package,
|
192
|
+
version: version,
|
193
|
+
release: release
|
194
|
+
)
|
195
|
+
end
|
196
|
+
|
197
|
+
fixed_versions.each do |package, versions|
|
198
|
+
entries[package] = Defect.new(
|
199
|
+
identifier: identifier,
|
200
|
+
description: description,
|
201
|
+
severity: severity,
|
202
|
+
fixed_in: versions,
|
203
|
+
link: link
|
204
|
+
)
|
205
|
+
end
|
206
|
+
entries
|
207
|
+
end
|
208
|
+
|
209
|
+
# Given a 'fixed in' version and a defect status, determine what version
|
210
|
+
# represents the status for comparison.
|
211
|
+
#
|
212
|
+
# If status is released, this simply returns the fixed argument.
|
213
|
+
# If status is not-affected, {Package::MIN_VERSION} is returned.
|
214
|
+
# If status is some other value, {Package::MAX_VERSION} is returned and the
|
215
|
+
# defect is considered to apply to all versions.
|
216
|
+
#
|
217
|
+
# @param fixed [String] version that the vuln src says a defect is fixed in
|
218
|
+
# @param status [String] the current status of the defect
|
219
|
+
#
|
220
|
+
# @return [String] version string
|
221
|
+
def choose_version(fixed, status)
|
222
|
+
return fixed if status == 'released'
|
223
|
+
return Package::MIN_VERSION if status == 'not-affected'
|
224
|
+
|
225
|
+
Package::MAX_VERSION
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|