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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +16 -0
  3. data/.rubocop.yml +30 -0
  4. data/.rubocop_todo.yml +42 -0
  5. data/.travis.yml +12 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG.md +8 -0
  8. data/CONTRIBUTING.md +60 -0
  9. data/Gemfile +8 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +103 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +7 -0
  14. data/bin/setup +8 -0
  15. data/config/default.yml +8 -0
  16. data/dobby.gemspec +58 -0
  17. data/lib/dobby.rb +51 -0
  18. data/lib/dobby/builtins.rb +17 -0
  19. data/lib/dobby/cli.rb +64 -0
  20. data/lib/dobby/configuration.rb +58 -0
  21. data/lib/dobby/database.rb +62 -0
  22. data/lib/dobby/defect.rb +74 -0
  23. data/lib/dobby/dpkg.rb +21 -0
  24. data/lib/dobby/error.rb +6 -0
  25. data/lib/dobby/flag_manager.rb +67 -0
  26. data/lib/dobby/flags.yml +8 -0
  27. data/lib/dobby/formatter/abstract_formatter.rb +25 -0
  28. data/lib/dobby/formatter/colorizable.rb +41 -0
  29. data/lib/dobby/formatter/formatter_set.rb +79 -0
  30. data/lib/dobby/formatter/json_formatter.rb +42 -0
  31. data/lib/dobby/formatter/simple_formatter.rb +54 -0
  32. data/lib/dobby/options.rb +149 -0
  33. data/lib/dobby/package.rb +156 -0
  34. data/lib/dobby/package_source/abstract_package_source.rb +17 -0
  35. data/lib/dobby/package_source/dpkg_status_file.rb +85 -0
  36. data/lib/dobby/runner.rb +152 -0
  37. data/lib/dobby/scanner.rb +128 -0
  38. data/lib/dobby/severity.rb +66 -0
  39. data/lib/dobby/strategy.rb +168 -0
  40. data/lib/dobby/update_response.rb +19 -0
  41. data/lib/dobby/version.rb +24 -0
  42. data/lib/dobby/vuln_source/abstract_vuln_source.rb +26 -0
  43. data/lib/dobby/vuln_source/debian.rb +166 -0
  44. data/lib/dobby/vuln_source/ubuntu.rb +229 -0
  45. 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