dobby 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|