dobby 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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