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,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
|