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,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
module Formatter
|
5
|
+
class FormatterSet < Array
|
6
|
+
BUILTIN_FORMATTERS = {
|
7
|
+
'simple' => SimpleFormatter,
|
8
|
+
'json' => JSONFormatter
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
FORMATTER_APIS = %i[started finished file_started].freeze
|
12
|
+
|
13
|
+
FORMATTER_APIS.each do |method_name|
|
14
|
+
define_method(method_name) do |*args|
|
15
|
+
each { |f| f.send(method_name, *args) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
@options = options
|
21
|
+
end
|
22
|
+
|
23
|
+
def file_finished(file, results)
|
24
|
+
each { |f| f.file_finished(file, results) }
|
25
|
+
results
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_formatter(type, output_path = nil)
|
29
|
+
if output_path
|
30
|
+
dir_path = File.dirname(output_path)
|
31
|
+
FileUtils.mkdir_p(dir_path) unless File.exist?(dir_path)
|
32
|
+
output = File.open(output_path, 'w')
|
33
|
+
else
|
34
|
+
output = $stdout
|
35
|
+
end
|
36
|
+
|
37
|
+
self << formatter_class(type).new(output, @options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def close_output_files
|
41
|
+
each do |formatter|
|
42
|
+
formatter.output.close if formatter.output.is_a?(File)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def formatter_class(formatter_type)
|
49
|
+
case formatter_type
|
50
|
+
when Class
|
51
|
+
formatter_type
|
52
|
+
when /\A[A-Z]/
|
53
|
+
custom_formatter_class(formatter_type)
|
54
|
+
else
|
55
|
+
builtin_formatter_class(formatter_type)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def builtin_formatter_class(specified_key)
|
60
|
+
matching = BUILTIN_FORMATTERS.keys.select do |key|
|
61
|
+
key.start_with?(specified_key)
|
62
|
+
end
|
63
|
+
|
64
|
+
raise %(No formatter for "#{specified_key}") if matching.empty?
|
65
|
+
raise %(Cannot determine formatter for "#{specified_key}") if matching.size > 1
|
66
|
+
|
67
|
+
BUILTIN_FORMATTERS[matching.first]
|
68
|
+
end
|
69
|
+
|
70
|
+
def custom_formatter_class(specified_class_name)
|
71
|
+
constant_names = specified_class_name.split('::')
|
72
|
+
constant_names.shift if constant_names.first.empty?
|
73
|
+
constant_names.reduce(Object) do |namespace, constant_name|
|
74
|
+
namespace.const_get(constant_name, false)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
module Formatter
|
5
|
+
# Outputs results as JSON
|
6
|
+
class JSONFormatter < AbstractFormatter
|
7
|
+
def started(_target_files)
|
8
|
+
@results = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def file_started(file)
|
12
|
+
@results[file] = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def file_finished(file, results)
|
16
|
+
return if results.empty?
|
17
|
+
|
18
|
+
@results[file] = each_completed_result(results)
|
19
|
+
end
|
20
|
+
|
21
|
+
def finished(_files)
|
22
|
+
output.puts(Oj.dump(@results, mode: :strict))
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def each_completed_result(results)
|
28
|
+
results.map do |package, defects|
|
29
|
+
{
|
30
|
+
package: package.name,
|
31
|
+
version: package.version,
|
32
|
+
defects: serialize_each_defect(defects)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def serialize_each_defect(defects)
|
38
|
+
defects.sort_by(&:severity).map(&:to_hash)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
module Formatter
|
5
|
+
# Outputs simple text, possibly colored
|
6
|
+
class SimpleFormatter < AbstractFormatter
|
7
|
+
include Colorizable
|
8
|
+
|
9
|
+
def file_finished(_file, results)
|
10
|
+
return if results.empty?
|
11
|
+
|
12
|
+
each_completed_result(results)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def each_completed_result(results)
|
18
|
+
results.each do |package, defects|
|
19
|
+
output.puts
|
20
|
+
print_package(package)
|
21
|
+
print_each_defect(defects)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def print_package(package)
|
26
|
+
output.puts(package)
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_each_defect(defects)
|
30
|
+
defects.sort_by(&:severity).each { |d| print_defect(d) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def print_defect(defect)
|
34
|
+
severity = colored_severity(defect.severity)
|
35
|
+
output.printf("\t%-25s %-10s\n", defect.identifier, severity)
|
36
|
+
end
|
37
|
+
|
38
|
+
def colored_severity(severity)
|
39
|
+
case severity
|
40
|
+
when Severity::Negligible, Severity::Low
|
41
|
+
green(severity)
|
42
|
+
when Severity::Medium
|
43
|
+
yellow(severity)
|
44
|
+
when Severity::High
|
45
|
+
magenta(severity)
|
46
|
+
when Severity::Critical
|
47
|
+
red(severity)
|
48
|
+
else
|
49
|
+
white(severity)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Adapted from rubocop's Options:
|
4
|
+
# Copyright (c) 2012-18 Bozhidar Batsov
|
5
|
+
# Additional modifications Copyright (c) 2018 Joe Truba
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
module Dobby
|
19
|
+
# Parsing of command line options and arguments.
|
20
|
+
class Options
|
21
|
+
EXITING_OPTIONS = %i[version verbose_version].freeze
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@options = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse(cli_args)
|
28
|
+
args = args_from_file.concat(args_from_env).concat(cli_args)
|
29
|
+
define_options.parse!(args)
|
30
|
+
args << '/var/lib/dpkg/status' if args.empty?
|
31
|
+
[@options, args]
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def args_from_file
|
37
|
+
if File.exist?('.dobby') && !File.directory?('.dobby')
|
38
|
+
IO.readlines('.dobby').map(&:strip)
|
39
|
+
else
|
40
|
+
[]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def args_from_env
|
45
|
+
Shellwords.split(ENV.fetch('DEBSECAN_OPTS', ''))
|
46
|
+
end
|
47
|
+
|
48
|
+
def define_options
|
49
|
+
OptionParser.new do |opts|
|
50
|
+
opts.program_name = 'meraki-dobby'
|
51
|
+
opts.banner = <<-BANNER.strip_indent
|
52
|
+
Usage: dobby [options] [file1, file2, ...]
|
53
|
+
dobby -o file [file1, file2, ...]
|
54
|
+
dobby -f simple -f json -o bar [file1, file2, ...]
|
55
|
+
|
56
|
+
BANNER
|
57
|
+
|
58
|
+
add_boolean_options(opts)
|
59
|
+
add_formatting_options(opts)
|
60
|
+
add_configuration_options(opts)
|
61
|
+
add_strategy_options(opts)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Sets a value in the @options hash, based on the given long option and its
|
66
|
+
# value, in addition to calling the block if a block is given.
|
67
|
+
def option(opts, *args)
|
68
|
+
long_opt_symbol = long_opt_symbol(args)
|
69
|
+
args += Array(OptionsHelp::TEXT[long_opt_symbol])
|
70
|
+
opts.on(*args) do |arg|
|
71
|
+
@options[long_opt_symbol] = arg
|
72
|
+
yield arg if block_given?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Finds the option in `args` starting with -- and converts it to a symbol,
|
77
|
+
# e.g. [..., '--auto-correct', ...] to :auto_correct.
|
78
|
+
def long_opt_symbol(args)
|
79
|
+
long_opt = args.find { |arg| arg.start_with?('--') }
|
80
|
+
long_opt[2..-1].sub('[no-]', '').sub(/ .*/, '')
|
81
|
+
.tr('-', '_').gsub(/[\[\]]/, '').to_sym
|
82
|
+
end
|
83
|
+
|
84
|
+
def add_formatting_options(opts)
|
85
|
+
option(opts, '-f', '--format FORMATTER') do |f|
|
86
|
+
@options[:formatters] ||= []
|
87
|
+
@options[:formatters] << [f]
|
88
|
+
end
|
89
|
+
|
90
|
+
option(opts, '-o', '--out FILE') do |path|
|
91
|
+
if @options[:formatters]
|
92
|
+
@options[:formatters].last << path
|
93
|
+
else
|
94
|
+
@options[:output_path] = path
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_boolean_options(opts)
|
100
|
+
option(opts, '--debug')
|
101
|
+
option(opts, '--fail-fast')
|
102
|
+
option(opts, '--list-target-files')
|
103
|
+
option(opts, '-v', '--version')
|
104
|
+
option(opts, '-V', '--verbose-version')
|
105
|
+
option(opts, '--[no-]color')
|
106
|
+
option(opts, '--[no-]fixed-only')
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_configuration_options(opts)
|
110
|
+
option(opts, '-P', '--package-source PACKAGE-SOURCE')
|
111
|
+
option(opts, '-S', '--vuln-source VULN-SOURCE')
|
112
|
+
end
|
113
|
+
|
114
|
+
def add_strategy_options(opts)
|
115
|
+
Builtins::ALL.each do |strategy|
|
116
|
+
strategy.cli_options.each do |opt|
|
117
|
+
option(opts, *opt)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
module OptionsHelp
|
124
|
+
TEXT = {
|
125
|
+
color: 'Force colored output on or off.',
|
126
|
+
format: ['Choose an output formatter. This option',
|
127
|
+
'can be specified multiple times to enable',
|
128
|
+
'multiple formatters at the same time.',
|
129
|
+
' [s]imple (default)',
|
130
|
+
' [j]son',
|
131
|
+
' custom formatter class name'],
|
132
|
+
fail_fast: 'Exit as soon as a defect is discovered.',
|
133
|
+
fixed_only: ['Only report vulnerabilities which have a fix',
|
134
|
+
'version noted in the vulnerability source.'],
|
135
|
+
list_target_files: ['List the package source files that would be inspected',
|
136
|
+
'and then exit.'],
|
137
|
+
out: ['Use with --format to instruct the previous formatter',
|
138
|
+
'to output to the specified path instead of to stdout.'],
|
139
|
+
package_source: ['Choose a package source.',
|
140
|
+
' [d]pkg (default)',
|
141
|
+
' custom package source class name'],
|
142
|
+
vuln_source: ['Choose a vulnerability source.',
|
143
|
+
' [d]ebian (default)',
|
144
|
+
' custom vulnerability source class name'],
|
145
|
+
version: 'Display version.',
|
146
|
+
verbose_version: 'Display verbose verison.'
|
147
|
+
}.freeze
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dobby
|
4
|
+
# A Package describes a particular Debian package installation.
|
5
|
+
# Dobby::Package is adapted from Debian::Deb and Debian::Field
|
6
|
+
# source: https://anonscm.debian.org/cgit/pkg-ruby-extras/ruby-debian.git/tree/lib/debian.rb
|
7
|
+
class Package
|
8
|
+
# MaxVersion is a special value which is always sorted first.
|
9
|
+
MAX_VERSION = '|MAX|'
|
10
|
+
|
11
|
+
# MinVersion is a special value which is always sorted last.
|
12
|
+
MIN_VERSION = '|MIN|'
|
13
|
+
|
14
|
+
# A required field was missing during initialization
|
15
|
+
class FieldRequiredError < Error
|
16
|
+
attr_reader :args, :field
|
17
|
+
def initialize(field)
|
18
|
+
super("Missing required field '#{field}'")
|
19
|
+
@field = field
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :name
|
24
|
+
attr_reader :version
|
25
|
+
attr_reader :release
|
26
|
+
attr_reader :source
|
27
|
+
attr_reader :dist
|
28
|
+
attr_reader :arch
|
29
|
+
attr_reader :target
|
30
|
+
attr_reader :multiarch
|
31
|
+
|
32
|
+
# Set up a new Debian Package
|
33
|
+
#
|
34
|
+
# @param name [String] Package name
|
35
|
+
# @param version [String] Package version
|
36
|
+
# @param source [String] Name of the source package, if applicable
|
37
|
+
#
|
38
|
+
# @raise [FieldRequiredError] if initialized without name or version
|
39
|
+
def initialize(name:, version:, release:, dist: nil, arch: nil, source: nil,
|
40
|
+
target: nil, multiarch: nil)
|
41
|
+
raise FieldRequiredError, 'name' unless name
|
42
|
+
raise FieldRequiredError, 'version' unless version
|
43
|
+
raise FieldRequiredError, 'release' unless release
|
44
|
+
raise FieldRequiredError, 'arch' if arch.nil? && multiarch == 'same'
|
45
|
+
|
46
|
+
@name = name
|
47
|
+
@version = version
|
48
|
+
@source = source
|
49
|
+
@dist = dist
|
50
|
+
@release = release
|
51
|
+
@arch = arch
|
52
|
+
@target = target
|
53
|
+
@multiarch = multiarch
|
54
|
+
end
|
55
|
+
|
56
|
+
# When a package has multiarch set to same, dpkg and apt will know of it by
|
57
|
+
# a name such as 'libquadmath0:amd64' instead of 'libquadmath0'. In these cases,
|
58
|
+
# return the name alongside the architecture to make it easier to act on results.
|
59
|
+
def apt_name
|
60
|
+
return name unless multiarch == 'same'
|
61
|
+
|
62
|
+
"#{name}:#{arch}"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Compared to some other {Package}, should this Package be filtered from results?
|
66
|
+
#
|
67
|
+
# If filter is set to :default, return true if releases do not match or if my
|
68
|
+
# version is at least the other package's version.
|
69
|
+
#
|
70
|
+
# If filter is set to :target, addtionally return true if my target version is
|
71
|
+
# at least the other's version.
|
72
|
+
#
|
73
|
+
# @param other [Package]
|
74
|
+
# @param filter [Symbol]
|
75
|
+
#
|
76
|
+
# @raise [UnknownFilterError] when given an unknown value for filter
|
77
|
+
def filtered?(other, filter = :default)
|
78
|
+
return true if release != other.release || self >= other
|
79
|
+
return target_at_least?(other) if filter == :target
|
80
|
+
return false if filter == :default
|
81
|
+
|
82
|
+
raise UnknownFilterError, filter
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [String] String representation of the package.
|
86
|
+
def to_s
|
87
|
+
"#{apt_name} #{version}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param version [Package]
|
91
|
+
#
|
92
|
+
# @return [Boolean] True if the target version meets or exceeds the provided
|
93
|
+
# package version
|
94
|
+
def target_at_least?(version)
|
95
|
+
!target.nil? && version.compare_to(target.to_s) <= 0
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param other [Package]
|
99
|
+
#
|
100
|
+
# @return [Boolean] True if other is present and other.package is the same as self.package
|
101
|
+
def ===(other)
|
102
|
+
other && (name == other.name || source == other.name || name == other.source)
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param other [Package]
|
106
|
+
# @return [Boolean] True if self === other and self.version is less than other.version
|
107
|
+
def <(other)
|
108
|
+
self === other && compare_to(other.version) < 0
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param other [Package]
|
112
|
+
# @return [Boolean] True if self === other and self.version is less than or
|
113
|
+
# equal to other.version
|
114
|
+
def <=(other)
|
115
|
+
self === other && compare_to(other.version) <= 0
|
116
|
+
end
|
117
|
+
|
118
|
+
# @param other [Package]
|
119
|
+
# @return [Boolean] True if self === other and self.version equals other.version
|
120
|
+
def ==(other)
|
121
|
+
self === other && compare_to(other.version).zero?
|
122
|
+
end
|
123
|
+
|
124
|
+
# @param other [Package]
|
125
|
+
# @return [Boolean] True if self === other and self.version is greater than or
|
126
|
+
# equal to other.version
|
127
|
+
def >=(other)
|
128
|
+
self === other && compare_to(other.version) >= 0
|
129
|
+
end
|
130
|
+
|
131
|
+
# @param other [Package]
|
132
|
+
# @return [Boolean] True if self === other and self.version is greater than other.version
|
133
|
+
def >(other)
|
134
|
+
self === other && compare_to(other.version) > 0
|
135
|
+
end
|
136
|
+
|
137
|
+
# @param other [Package]
|
138
|
+
# @return [Boolean] True if self === other and self.version does not equal other.version
|
139
|
+
def !=(other)
|
140
|
+
self === other && compare_to(other.version) != 0
|
141
|
+
end
|
142
|
+
|
143
|
+
# This method is wrapped by the standard comparison operators, but is provided for cases where
|
144
|
+
# it is not practical to compare two Package objects.
|
145
|
+
#
|
146
|
+
# @param other [String] Version string
|
147
|
+
# @return [Integer]
|
148
|
+
def compare_to(other)
|
149
|
+
return 0 if version == other
|
150
|
+
return -1 if version == MIN_VERSION || other == MAX_VERSION
|
151
|
+
return 1 if version == MAX_VERSION || other == MIN_VERSION
|
152
|
+
|
153
|
+
Debian::AptPkg.cmp_version(version, other)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|