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,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
|
data/lib/dobby/runner.rb
ADDED
@@ -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
|