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