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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dobby
4
+ module PackageSource
5
+ # @abstract Subclass and override {#parse} to implement a custom Package
6
+ # source.
7
+ class AbstractPackageSource
8
+ include Dobby::Strategy
9
+
10
+ # All logic for creating an Array<Package>]
11
+ # @return [Array<Package>]
12
+ def parse
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dobby
4
+ module PackageSource
5
+ # Defines a strategy for creating a {Package} array from /var/lib/dpkg/status
6
+ # or a similarly formatted file.
7
+ class DpkgStatusFile < AbstractPackageSource
8
+ args %i[file_path dist release]
9
+
10
+ option :file_path, '/var/lib/dpkg/status'
11
+ option :dist, 'Debian'
12
+ option :release, Dpkg.code_name
13
+
14
+ # A Dpkg section has unexpected formatting
15
+ class DpkgFormatError < Error; end
16
+
17
+ # rubocop:disable Layout/AlignArray
18
+ def self.cli_options
19
+ [
20
+ ['--release NAME', 'Release code name for package definitions.',
21
+ 'Defaults to the code name of the current system.'],
22
+ ['--dist DIST', 'The full name of the distribution for package definitions.',
23
+ 'Defaults to "Debian".']
24
+ ]
25
+ end
26
+ # rubocop:enable Layout/AlignArray
27
+
28
+ # @return [Array<Package>]
29
+ def parse
30
+ packages = []
31
+ File.read(options.file_path).split("\n\n").each do |section|
32
+ begin
33
+ packages << package_from_section(section)
34
+ rescue Package::FieldRequiredError => e
35
+ # If the Version field is missing, this is probably a virtual
36
+ # or meta-package (e.g. little-table-dev) - Skip it. Name and
37
+ # release should never be missing, so reraise the error in those
38
+ # cases.
39
+ next if e.field == 'version'
40
+
41
+ raise
42
+ end
43
+ end
44
+ packages
45
+ end
46
+
47
+ private
48
+
49
+ # @param section [String]
50
+ # @return [Package]
51
+ # @raise [FormatError]
52
+ def package_from_section(section)
53
+ pkg = {}
54
+ field = nil
55
+
56
+ section.each_line do |line|
57
+ line.chomp!
58
+ if /^\s/ =~ line
59
+ raise FormatError, "Unexpected whitespace at start of line: '#{line}'" unless field
60
+
61
+ pkg[field] += "\n" + line
62
+ elsif /(^\S+):\s*(.*)/ =~ line
63
+ field = Regexp.last_match(1).capitalize
64
+ if pkg.key?(field)
65
+ raise FormatError, "Unexpected duplicate field '#{field}' in line '#{line}'"
66
+ end
67
+
68
+ value = Regexp.last_match(2).strip
69
+ value = value.split[0] if field == 'Source'
70
+ pkg[field] = value
71
+ end
72
+ end
73
+ Package.new(
74
+ name: pkg['Package'],
75
+ version: pkg['Version'],
76
+ dist: options.dist,
77
+ release: options.release,
78
+ arch: pkg['Architecture'],
79
+ source: pkg['Source'],
80
+ multiarch: pkg['Multi-Arch']
81
+ )
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dobby
4
+ class Runner
5
+ attr_reader :errors, :aborting
6
+ alias aborting? aborting
7
+
8
+ def initialize(options)
9
+ @options = options
10
+ @errors = []
11
+ @aborting = false
12
+ end
13
+
14
+ def run(paths)
15
+ target_files = resolve_paths(paths)
16
+ return list_files(target_files) if @options[:list_target_files]
17
+
18
+ load_vuln_source(@options[:vuln_source])
19
+ load_package_source(@options[:package_source])
20
+ load_scanner
21
+ inspect_files(target_files)
22
+ end
23
+
24
+ private
25
+
26
+ def inspect_files(files)
27
+ inspected = []
28
+
29
+ formatter_set.started(files)
30
+
31
+ each_inspected_file(files) { |file| inspected << file }
32
+ ensure
33
+ formatter_set.finished(inspected.freeze)
34
+ formatter_set.close_output_files
35
+ end
36
+
37
+ def resolve_paths(target_files)
38
+ target_files.map { |f| File.expand_path(f) }.freeze
39
+ end
40
+
41
+ def each_inspected_file(files)
42
+ files.reduce(true) do |passed, file|
43
+ break false if aborting?
44
+
45
+ results = process_file(file)
46
+
47
+ yield file
48
+
49
+ if results.any?
50
+ break false if @options[:fail_fast]
51
+
52
+ next false
53
+ end
54
+
55
+ passed
56
+ end
57
+ end
58
+
59
+ def formatter_set
60
+ @formatter_set ||= begin
61
+ set = Formatter::FormatterSet.new(@options)
62
+ pairs = @options[:formatters] || [['simple', @options[:output_path]]]
63
+ pairs.each do |formatter_key, output_path|
64
+ set.add_formatter(formatter_key, output_path)
65
+ end
66
+ set
67
+ end
68
+ end
69
+
70
+ def load_package_source(selected)
71
+ selected ||= 'dpkg'
72
+ @package_source = package_source_loader(selected)
73
+ end
74
+
75
+ def load_vuln_source(selected)
76
+ selected ||= 'debian'
77
+ @vuln_source = vuln_source_loader(selected)
78
+ end
79
+
80
+ def load_scanner
81
+ vuln_source = @vuln_source.new(@options)
82
+ @database = Dobby::Database.new(vuln_source)
83
+ @scanner = Scanner.new(nil, @database)
84
+ end
85
+
86
+ def process_file(file = nil)
87
+ formatter_set.file_started(file)
88
+ source = @package_source.new(@options.merge(file_path: file))
89
+ packages = source.parse
90
+ results = run_scanner(packages)
91
+ formatter_set.file_finished(file, results)
92
+ results
93
+ end
94
+
95
+ def run_scanner(packages)
96
+ @scanner.packages = packages
97
+ @scanner.scan(defect_filter: Scanner::DEFECT_FILTER_FIXED)
98
+ end
99
+
100
+ def fuzzy_source_name(key, sources)
101
+ sources.keys.select { |k| k.start_with?(key) }
102
+ end
103
+
104
+ def builtin_package_source_class(specified)
105
+ matching = fuzzy_source_name(specified, Builtins::PACKAGE_SOURCES)
106
+
107
+ raise %(No package source for "#{specified}") if matching.empty?
108
+ raise %(Ambiguous package source for "#{specified}") if matching.size > 1
109
+
110
+ Builtins::PACKAGE_SOURCES[matching.first]
111
+ end
112
+
113
+ def builtin_vuln_source_class(specified)
114
+ matching = fuzzy_source_name(specified, Builtins::VULN_SOURCES)
115
+
116
+ raise %(No vulnerability source for "#{specified}") if matching.empty?
117
+ raise %(Ambiguous vulnerability source for "#{specified}") if matching.size > 1
118
+
119
+ Builtins::VULN_SOURCES[matching.first]
120
+ end
121
+
122
+ def vuln_source_loader(specifier)
123
+ case specifier
124
+ when Class
125
+ specifier
126
+ when /\A[A-Z]/
127
+ custom_class(specifier)
128
+ else
129
+ builtin_vuln_source_class(specifier)
130
+ end
131
+ end
132
+
133
+ def package_source_loader(specifier)
134
+ case specifier
135
+ when Class
136
+ specifier
137
+ when /\A[A-Z]/
138
+ custom_class(specifier)
139
+ else
140
+ builtin_package_source_class(specifier)
141
+ end
142
+ end
143
+
144
+ def custom_class(name)
145
+ constant_names = name.split('::')
146
+ constant_names.shift if constant_names.first.empty?
147
+ constant_names.reduce(Object) do |namespace, constant_name|
148
+ namespace.const_get(constant_name, false)
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dobby
4
+ # Compares a {Database} and {Array<Package>} to discover what defects
5
+ # affect a system.
6
+ class Scanner
7
+ attr_reader :database, :packages, :results
8
+
9
+ FLAG_FILTER_ALL = :all
10
+ FLAG_FILTER_ALLOWED = :allowed
11
+ FLAG_FILTER_WHITELISTED = :whitelisted
12
+ FLAG_FILTER_DEFAULT = :default
13
+
14
+ DEFECT_FILTER_DEFAULT = :default
15
+ DEFECT_FILTER_FIXED = :only_fixed
16
+
17
+ VERSION_FILTER_TARGET = :target
18
+ VERSION_FILTER_DEFAULT = :default
19
+
20
+ # @param packages [Array<Package>]
21
+ # @param database [Database]
22
+ def initialize(packages, database)
23
+ @packages = packages
24
+ @database = database
25
+
26
+ @results = Hash.new { |h, k| h[k] = [] }
27
+ end
28
+
29
+ # Whenever the package set the scanner is configured with change,
30
+ # the stored results will be wiped.
31
+ def packages=(arr)
32
+ @results.clear
33
+ @packages = arr
34
+ end
35
+
36
+ # Determine which packages are affected by which defects.
37
+ #
38
+ # @option [Symbol] :defect_filter {DEFECT_FILTER_DEFAULT}
39
+ # - {DEFECT_FILTER_DEFAULT}
40
+ # Apply no special filters to defects in the database
41
+ # - {DEFECT_FILTER_FIXED}
42
+ # Only include defects that have a fix available
43
+ # @option [Symbol] :flag_filter {FLAG_FILTER_DEFAULT}
44
+ # - {FLAG_FILTER_ALL}
45
+ # Apply no filter at all to the results
46
+ # - {FLAG_FILTER_DEFAULT}
47
+ # Ignore results marked with any flag
48
+ # - {FLAG_FILTER_ALLOWED}
49
+ # Include only results flagged allowed
50
+ # - {FLAG_FILTER_WHITELISTED}
51
+ # Include only results flagged whitelisted
52
+ # @option [Symbol] :version_filter {VERSION_FILTER_DEFAULT}
53
+ # - {VERSION_FILTER_DEFAULT}
54
+ # For a given package and defect, ensure that the release of the defect's
55
+ # fix versions match the package and that the version of the package
56
+ # is less than the fix version
57
+ # - {VERSION_FILTER_TARGET}
58
+ # See {#scan_by_target} for more information on how this complex filter behaves.
59
+ #
60
+ # Order of filter processing, from first to last:
61
+ # 1. Flag
62
+ # 2. Defect
63
+ # 3. Version
64
+ def scan(defect_filter: DEFECT_FILTER_DEFAULT,
65
+ flag_filter: FLAG_FILTER_DEFAULT,
66
+ version_filter: VERSION_FILTER_DEFAULT)
67
+ @results.clear
68
+ packages.each do |package|
69
+ scan_results = scan_one(package: package,
70
+ defect_filter: defect_filter,
71
+ flag_filter: flag_filter,
72
+ version_filter: version_filter)
73
+ @results.merge! scan_results
74
+ end
75
+ results
76
+ end
77
+
78
+ # For a given package, determine which packages affect it, if any.
79
+ def scan_one(package:,
80
+ defect_filter: DEFECT_FILTER_DEFAULT,
81
+ flag_filter: FLAG_FILTER_DEFAULT,
82
+ version_filter: VERSION_FILTER_DEFAULT)
83
+ res = Hash.new { |h, k| h[k] = [] }
84
+ database.defects_for(package).each do |defect|
85
+ next if defect.filtered?(filter: defect_filter, flag_filter: flag_filter)
86
+
87
+ defect.fixed_in.each do |v|
88
+ next if package.filtered?(v, version_filter)
89
+
90
+ res[package] << defect
91
+ end
92
+ end
93
+ res
94
+ end
95
+
96
+ # Determine which defects are resolved by upgrading to a target version.
97
+ #
98
+ # Given:
99
+ # - A Package at version 1 and a target version of 3
100
+ # - A Defect, D1, with a fix version of 2
101
+ # - A Defect, D2, with a fix version of 3
102
+ # - A Defect, D3, with a fix version of 4
103
+ #
104
+ # Returns D1 and D2, but not D3.
105
+ #
106
+ # This is a use-specific wrapper around {#scan}
107
+ #
108
+ # @note Packages that do not have a target version set are skipped.
109
+ def scan_by_target
110
+ scan(defect_filter: DEFECT_FILTER_FIXED,
111
+ flag_filter: FLAG_FILTER_DEFAULT,
112
+ version_filter: VERSION_FILTER_TARGET)
113
+ end
114
+
115
+ # For a specific package, determine which defects are resolved by upgrading
116
+ # to its' target version.
117
+ #
118
+ # This is a use-specific wrapper around {scan_one}
119
+ #
120
+ # @param [Package]
121
+ def scan_one_by_target(package)
122
+ scan_one(package: package,
123
+ defect_filter: DEFECT_FILTER_FIXED,
124
+ flag_filter: FLAG_FILTER_DEFAULT,
125
+ version_filter: VERSION_FILTER_TARGET)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dobby
4
+ # Standardized definitions for severity categories
5
+ module Severity
6
+ # Implements a custom <=> method so that severities can be sorted as
7
+ # we see fit. In particular, these objects sort based on their index
8
+ # in the {SEVERITIES} array.
9
+ class Severity
10
+ attr_reader :value
11
+ def initialize(value)
12
+ @value = value
13
+ end
14
+
15
+ def to_s
16
+ @value
17
+ end
18
+
19
+ def ==(other)
20
+ value == other.value
21
+ end
22
+
23
+ def <=>(other)
24
+ return 0 if other == self
25
+ return 1 if SEVERITIES.index(self) < SEVERITIES.index(other)
26
+
27
+ -1
28
+ end
29
+
30
+ def <(other)
31
+ SEVERITIES.index(self) < SEVERITIES.index(other)
32
+ end
33
+ end
34
+
35
+ # A defect which has not yet been assigned a priority or we do not have
36
+ # a translation for.
37
+ Unknown = Severity.new('Unknown')
38
+
39
+ # Technically a security issue, but has no real damage, extremely strict
40
+ # requirements, or other constraints that nullify impact.
41
+ Negligible = Severity.new('Negligible')
42
+
43
+ # Security problem, but difficult to exploit, requires user assistance
44
+ # or does very little damage.
45
+ Low = Severity.new('Low')
46
+
47
+ # "Real" security problem that is generally exploitable.
48
+ Medium = Severity.new('Medium')
49
+
50
+ # Real "problem", that is generally exploitable in a default configuration.
51
+ High = Severity.new('High')
52
+
53
+ # The world is on fire, send help!
54
+ Critical = Severity.new('Critical')
55
+
56
+ # All severities in an ordered list
57
+ SEVERITIES = [
58
+ Unknown,
59
+ Negligible,
60
+ Low,
61
+ Medium,
62
+ High,
63
+ Critical
64
+ ].freeze
65
+ end
66
+ end