bundler-audit-ng 0.6.1

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.
@@ -0,0 +1 @@
1
+ 2017-06-13 16:51:56 UTC
@@ -0,0 +1,14 @@
1
+ name: bundler-audit-ng
2
+ summary: Patch-level verification for Bundler
3
+ description: bundler-audit provides patch-level verification for Bundled apps.
4
+ license: GPL-3.0+
5
+ authors: Postmodern, pxlpnk
6
+ email: rubygems@an-ti.eu
7
+ homepage: https://github.com/pxlpnk/bundler-audit-ng
8
+
9
+ required_ruby_version: ">= 1.9.3"
10
+ required_rubygems_version: ">= 1.8.0"
11
+
12
+ dependencies:
13
+ thor: ~> 0.18
14
+ bundler: ">= 1.2.0, < 3"
@@ -0,0 +1,19 @@
1
+ #
2
+ # Copyright (c) 2013-2019 Hal Brodigan (postmodern.mod3 at gmail.com)
3
+ #
4
+ # bundler-audit is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # bundler-audit is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ require 'bundler/audit/database'
19
+ require 'bundler/audit/version'
@@ -0,0 +1,177 @@
1
+ #
2
+ # Copyright (c) 2013-2019 Hal Brodigan (postmodern.mod3 at gmail.com)
3
+ #
4
+ # bundler-audit is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # bundler-audit is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ require 'yaml'
19
+
20
+ module Bundler
21
+ module Audit
22
+ class Advisory < Struct.new(:path,
23
+ :id,
24
+ :url,
25
+ :title,
26
+ :date,
27
+ :description,
28
+ :cvss_v2,
29
+ :cve,
30
+ :osvdb,
31
+ :ghsa,
32
+ :unaffected_versions,
33
+ :patched_versions)
34
+
35
+ #
36
+ # Loads the advisory from a YAML file.
37
+ #
38
+ # @param [String] path
39
+ # The path to the advisory YAML file.
40
+ #
41
+ # @return [Advisory]
42
+ #
43
+ # @api semipublic
44
+ #
45
+ def self.load(path)
46
+ id = File.basename(path).chomp('.yml')
47
+ data = YAML.load_file(path)
48
+
49
+ unless data.kind_of?(Hash)
50
+ raise("advisory data in #{path.dump} was not a Hash")
51
+ end
52
+
53
+ parse_versions = lambda { |versions|
54
+ Array(versions).map do |version|
55
+ Gem::Requirement.new(*version.split(', '))
56
+ end
57
+ }
58
+
59
+ return new(
60
+ path,
61
+ id,
62
+ data['url'],
63
+ data['title'],
64
+ data['date'],
65
+ data['description'],
66
+ data['cvss_v2'],
67
+ data['cve'],
68
+ data['osvdb'],
69
+ data['ghsa'],
70
+ parse_versions[data['unaffected_versions']],
71
+ parse_versions[data['patched_versions']]
72
+ )
73
+ end
74
+
75
+ #
76
+ # The CVE identifier.
77
+ #
78
+ # @return [String, nil]
79
+ #
80
+ def cve_id
81
+ "CVE-#{cve}" if cve
82
+ end
83
+
84
+ #
85
+ # The OSVDB identifier.
86
+ #
87
+ # @return [String, nil]
88
+ #
89
+ def osvdb_id
90
+ "OSVDB-#{osvdb}" if osvdb
91
+ end
92
+
93
+ #
94
+ # The GHSA (GitHub Security Advisory) identifier
95
+ #
96
+ # @return [String, nil]
97
+ #
98
+ def ghsa_id
99
+ "GHSA-#{ghsa}" if ghsa
100
+ end
101
+
102
+ #
103
+ # Return a compacted list of all ids
104
+ def identifiers
105
+ [
106
+ cve_id,
107
+ osvdb_id,
108
+ ghsa_id
109
+ ].compact
110
+ end
111
+
112
+ #
113
+ # Determines how critical the vulnerability is.
114
+ #
115
+ # @return [:low, :medium, :high]
116
+ # The criticality of the vulnerability based on the CVSSv2 score.
117
+ #
118
+ def criticality
119
+ case cvss_v2
120
+ when 0.0..3.3 then :low
121
+ when 3.3..6.6 then :medium
122
+ when 6.6..10.0 then :high
123
+ end
124
+ end
125
+
126
+ #
127
+ # Checks whether the version is not affected by the advisory.
128
+ #
129
+ # @param [Gem::Version] version
130
+ # The version to compare against {#unaffected_versions}.
131
+ #
132
+ # @return [Boolean]
133
+ # Specifies whether the version is not affected by the advisory.
134
+ #
135
+ # @since 0.2.0
136
+ #
137
+ def unaffected?(version)
138
+ unaffected_versions.any? do |unaffected_version|
139
+ unaffected_version === version
140
+ end
141
+ end
142
+
143
+ #
144
+ # Checks whether the version is patched against the advisory.
145
+ #
146
+ # @param [Gem::Version] version
147
+ # The version to compare against {#patched_versions}.
148
+ #
149
+ # @return [Boolean]
150
+ # Specifies whether the version is patched against the advisory.
151
+ #
152
+ # @since 0.2.0
153
+ #
154
+ def patched?(version)
155
+ patched_versions.any? do |patched_version|
156
+ patched_version === version
157
+ end
158
+ end
159
+
160
+ #
161
+ # Checks whether the version is vulnerable to the advisory.
162
+ #
163
+ # @param [Gem::Version] version
164
+ # The version to compare against {#patched_versions}.
165
+ #
166
+ # @return [Boolean]
167
+ # Specifies whether the version is vulnerable to the advisory or not.
168
+ #
169
+ def vulnerable?(version)
170
+ !patched?(version) && !unaffected?(version)
171
+ end
172
+
173
+ alias to_s id
174
+
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,155 @@
1
+ #
2
+ # Copyright (c) 2013-2019 Hal Brodigan (postmodern.mod3 at gmail.com)
3
+ #
4
+ # bundler-audit is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # bundler-audit is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ require 'bundler/audit/scanner'
19
+ require 'bundler/audit/version'
20
+
21
+ require 'thor'
22
+ require 'bundler'
23
+ require 'bundler/vendored_thor'
24
+
25
+ module Bundler
26
+ module Audit
27
+ class CLI < ::Thor
28
+
29
+ default_task :check
30
+ map '--version' => :version
31
+
32
+ desc 'check', 'Checks the Gemfile.lock for insecure dependencies'
33
+ method_option :quiet, :type => :boolean, :aliases => '-q'
34
+ method_option :verbose, :type => :boolean, :aliases => '-v'
35
+ method_option :ignore, :type => :array, :aliases => '-i'
36
+ method_option :update, :type => :boolean, :aliases => '-u'
37
+
38
+ def check
39
+ update if options[:update]
40
+
41
+ scanner = Scanner.new
42
+ vulnerable = false
43
+
44
+ scanner.scan(:ignore => options.ignore) do |result|
45
+ vulnerable = true
46
+
47
+ case result
48
+ when Scanner::InsecureSource
49
+ print_warning "Insecure Source URI found: #{result.source}"
50
+ when Scanner::UnpatchedGem
51
+ print_advisory result.gem, result.advisory
52
+ end
53
+ end
54
+
55
+ if vulnerable
56
+ say "Vulnerabilities found!", :red
57
+ exit 1
58
+ else
59
+ say("No vulnerabilities found", :green) unless options.quiet?
60
+ end
61
+ end
62
+
63
+ desc 'update', 'Updates the ruby-advisory-db'
64
+ method_option :quiet, :type => :boolean, :aliases => '-q'
65
+
66
+ def update
67
+ say("Updating ruby-advisory-db ...") unless options.quiet?
68
+
69
+ case Database.update!(quiet: options.quiet?)
70
+ when true
71
+ say("Updated ruby-advisory-db", :green) unless options.quiet?
72
+ when false
73
+ say "Failed updating ruby-advisory-db!", :red
74
+ exit 1
75
+ when nil
76
+ say "Skipping update", :yellow
77
+ end
78
+
79
+ unless options.quiet?
80
+ puts("ruby-advisory-db: #{Database.new.size} advisories")
81
+ end
82
+ end
83
+
84
+ desc 'version', 'Prints the bundler-audit version'
85
+ def version
86
+ database = Database.new
87
+
88
+ puts "#{File.basename($0)} #{VERSION} (advisories: #{database.size})"
89
+ end
90
+
91
+ protected
92
+
93
+ def say(message="", color=nil)
94
+ color = nil unless $stdout.tty?
95
+ super(message.to_s, color)
96
+ end
97
+
98
+ def print_warning(message)
99
+ say message, :yellow
100
+ end
101
+
102
+ def print_advisory(gem, advisory)
103
+ say "Name: ", :red
104
+ say gem.name
105
+
106
+ say "Version: ", :red
107
+ say gem.version
108
+
109
+ say "Advisory: ", :red
110
+
111
+ if advisory.cve
112
+ say advisory.cve_id
113
+ elsif advisory.osvdb
114
+ say advisory.osvdb_id
115
+ elsif advisory.ghsa
116
+ say advisory.ghsa_id
117
+ end
118
+
119
+ say "Criticality: ", :red
120
+ case advisory.criticality
121
+ when :low then say "Low"
122
+ when :medium then say "Medium", :yellow
123
+ when :high then say "High", [:red, :bold]
124
+ else say "Unknown"
125
+ end
126
+
127
+ say "URL: ", :red
128
+ say advisory.url
129
+
130
+ if options.verbose?
131
+ say "Description:", :red
132
+ say
133
+
134
+ print_wrapped advisory.description, :indent => 2
135
+ say
136
+ else
137
+
138
+ say "Title: ", :red
139
+ say advisory.title
140
+ end
141
+
142
+ unless advisory.patched_versions.empty?
143
+ say "Solution: upgrade to ", :red
144
+ say advisory.patched_versions.join(', ')
145
+ else
146
+ say "Solution: ", :red
147
+ say "remove or disable this gem until a patch is available!", [:red, :bold]
148
+ end
149
+
150
+ say
151
+ end
152
+
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,248 @@
1
+ #
2
+ # Copyright (c) 2013-2019 Hal Brodigan (postmodern.mod3 at gmail.com)
3
+ #
4
+ # bundler-audit is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # bundler-audit is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with bundler-audit. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ require 'bundler/audit/advisory'
19
+
20
+ require 'time'
21
+ require 'yaml'
22
+
23
+ module Bundler
24
+ module Audit
25
+ #
26
+ # Represents the directory of advisories, grouped by gem name
27
+ # and CVE number.
28
+ #
29
+ class Database
30
+
31
+ # Git URL of the ruby-advisory-db
32
+ URL = 'https://github.com/rubysec/ruby-advisory-db.git'
33
+
34
+ # Default path to the ruby-advisory-db
35
+ VENDORED_PATH = File.expand_path(File.join(File.dirname(__FILE__),'..','..','..','data','ruby-advisory-db'))
36
+
37
+ # Timestamp for when the database was last updated
38
+ VENDORED_TIMESTAMP = Time.parse(File.read("#{VENDORED_PATH}.ts")).utc
39
+
40
+ # Path to the user's copy of the ruby-advisory-db
41
+ USER_PATH = File.expand_path(File.join(ENV['HOME'],'.local','share','ruby-advisory-db'))
42
+
43
+ # The path to the advisory database
44
+ attr_reader :path
45
+
46
+ #
47
+ # Initializes the Advisory Database.
48
+ #
49
+ # @param [String] path
50
+ # The path to the advisory database.
51
+ #
52
+ # @raise [ArgumentError]
53
+ # The path was not a directory.
54
+ #
55
+ def initialize(path=self.class.path)
56
+ unless File.directory?(path)
57
+ raise(ArgumentError,"#{path.dump} is not a directory")
58
+ end
59
+
60
+ @path = path
61
+ end
62
+
63
+ #
64
+ # The default path for the database.
65
+ #
66
+ # @return [String]
67
+ # The path to the database directory.
68
+ #
69
+ def self.path
70
+ if File.directory?(USER_PATH)
71
+ t1 = Dir.chdir(USER_PATH) { Time.parse(`git log --date=iso8601 --pretty="%cd" -1`) }
72
+ t2 = VENDORED_TIMESTAMP
73
+
74
+ if t1 >= t2 then USER_PATH
75
+ else VENDORED_PATH
76
+ end
77
+ else
78
+ VENDORED_PATH
79
+ end
80
+ end
81
+
82
+ #
83
+ # Updates the ruby-advisory-db.
84
+ #
85
+ # @param [Boolean, quiet]
86
+ # Specify whether `git` should be `--quiet`.
87
+ #
88
+ # @return [Boolean, nil]
89
+ # Specifies whether the update was successful.
90
+ # A `nil` indicates no update was performed.
91
+ #
92
+ # @note
93
+ # Requires network access.
94
+ #
95
+ # @since 0.3.0
96
+ #
97
+ def self.update!(options={})
98
+ raise "Invalid option(s)" unless (options.keys - [:quiet]).empty?
99
+ if File.directory?(USER_PATH)
100
+ if File.directory?(File.join(USER_PATH, ".git"))
101
+ Dir.chdir(USER_PATH) do
102
+ command = %w(git pull --no-rebase)
103
+ command << '--quiet' if options[:quiet]
104
+ command << 'origin' << 'master'
105
+ system *command
106
+ end
107
+ end
108
+ else
109
+ command = %w(git clone)
110
+ command << '--quiet' if options[:quiet]
111
+ command << URL << USER_PATH
112
+ system *command
113
+ end
114
+ end
115
+
116
+ #
117
+ # Enumerates over every advisory in the database.
118
+ #
119
+ # @yield [advisory]
120
+ # If a block is given, it will be passed each advisory.
121
+ #
122
+ # @yieldparam [Advisory] advisory
123
+ # An advisory from the database.
124
+ #
125
+ # @return [Enumerator]
126
+ # If no block is given, an Enumerator will be returned.
127
+ #
128
+ def advisories(&block)
129
+ return enum_for(__method__) unless block_given?
130
+
131
+ each_advisory_path do |path|
132
+ yield Advisory.load(path)
133
+ end
134
+ end
135
+
136
+ #
137
+ # Enumerates over advisories for the given gem.
138
+ #
139
+ # @param [String] name
140
+ # The gem name to lookup.
141
+ #
142
+ # @yield [advisory]
143
+ # If a block is given, each advisory for the given gem will be yielded.
144
+ #
145
+ # @yieldparam [Advisory] advisory
146
+ # An advisory for the given gem.
147
+ #
148
+ # @return [Enumerator]
149
+ # If no block is given, an Enumerator will be returned.
150
+ #
151
+ def advisories_for(name)
152
+ return enum_for(__method__,name) unless block_given?
153
+
154
+ each_advisory_path_for(name) do |path|
155
+ yield Advisory.load(path)
156
+ end
157
+ end
158
+
159
+ #
160
+ # Verifies whether the gem is effected by any advisories.
161
+ #
162
+ # @param [Gem::Specification] gem
163
+ # The gem to verify.
164
+ #
165
+ # @yield [advisory]
166
+ # If a block is given, it will be passed advisories that effect
167
+ # the gem.
168
+ #
169
+ # @yieldparam [Advisory] advisory
170
+ # An advisory that effects the specific version of the gem.
171
+ #
172
+ # @return [Enumerator]
173
+ # If no block is given, an Enumerator will be returned.
174
+ #
175
+ def check_gem(gem)
176
+ return enum_for(__method__,gem) unless block_given?
177
+
178
+ advisories_for(gem.name) do |advisory|
179
+ if advisory.vulnerable?(gem.version)
180
+ yield advisory
181
+ end
182
+ end
183
+ end
184
+
185
+ #
186
+ # The number of advisories within the database.
187
+ #
188
+ # @return [Integer]
189
+ # The number of advisories.
190
+ #
191
+ def size
192
+ each_advisory_path.count
193
+ end
194
+
195
+ #
196
+ # Converts the database to a String.
197
+ #
198
+ # @return [String]
199
+ # The path to the database.
200
+ #
201
+ def to_s
202
+ @path
203
+ end
204
+
205
+ #
206
+ # Inspects the database.
207
+ #
208
+ # @return [String]
209
+ # The inspected database.
210
+ #
211
+ def inspect
212
+ "#<#{self.class}:#{self}>"
213
+ end
214
+
215
+ protected
216
+
217
+ #
218
+ # Enumerates over every advisory path in the database.
219
+ #
220
+ # @yield [path]
221
+ # The given block will be passed each advisory path.
222
+ #
223
+ # @yieldparam [String] path
224
+ # A path to an advisory `.yml` file.
225
+ #
226
+ def each_advisory_path(&block)
227
+ Dir.glob(File.join(@path,'gems','*','*.yml'),&block)
228
+ end
229
+
230
+ #
231
+ # Enumerates over the advisories for the given gem.
232
+ #
233
+ # @param [String] name
234
+ # The gem of the gem.
235
+ #
236
+ # @yield [path]
237
+ # The given block will be passed each advisory path.
238
+ #
239
+ # @yieldparam [String] path
240
+ # A path to an advisory `.yml` file.
241
+ #
242
+ def each_advisory_path_for(name,&block)
243
+ Dir.glob(File.join(@path,'gems',name,'*.yml'),&block)
244
+ end
245
+
246
+ end
247
+ end
248
+ end