redmine_audit 0.1.1 → 0.2.0

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.
data/init.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  Redmine::Plugin.register :redmine_audit do
2
2
  name 'Redmine Audit plugin'
3
3
  author 'Sho Hashimoto'
4
- description 'Redmine plugin for checking vulnerabilities'
4
+ description 'Redmine plugin for checking Redmine\'s own vulnerabilities'
5
5
  version RedmineAudit::VERSION
6
+ requires_redmine version_or_higher: '3.3.0'
6
7
  url 'https://github.com/sho-h/redmine_audit/'
7
8
  author_url 'https://github.com/sho-h/'
8
9
  end
@@ -1,13 +1,23 @@
1
1
  module RedmineAudit
2
- # Redmine advisory
2
+ # Redmine/Redmine plugin advisory
3
3
  #
4
4
  # ex) Advisory.new('High', 'http://someurl.example.com',
5
- # [Gem::Requirement], [Gem::Requirement])
6
- class Advisory < Struct.new(:severity,
7
- :details,
8
- :external_references,
9
- :unaffected_versions,
10
- :fixed_versions)
5
+ # [Gem::Requirement], [Gem::Requirement], 'CVE-XXXX-XXXX', nil, 7.5)
6
+ Advisory = Struct.new(:severity,
7
+ :details,
8
+ :external_references,
9
+ :unaffected_versions,
10
+ :fixed_versions,
11
+ :id, :cvss_v2, :cvss_v3) do
12
+ def initialize(severity, details, external_references,
13
+ unaffected_versions, fixed_versions, id, cvss_v2, cvss_v3)
14
+ super
15
+ self.severity = cvss_to_severity if self.severity.nil?
16
+ [:external_references, :unaffected_versions, :fixed_versions].each do |attr|
17
+ self[attr] = [] if self[attr].nil?
18
+ end
19
+ end
20
+
11
21
  # Checks whether the version is not affected by the advisory.
12
22
  #
13
23
  # @param [Gem::Version] version
@@ -44,5 +54,37 @@ module RedmineAudit
44
54
  def vulnerable?(version)
45
55
  !fixed?(version) && !unaffected?(version)
46
56
  end
57
+
58
+ private
59
+
60
+ def cvss_to_severity
61
+ if cvss_v2
62
+ case cvss_v2
63
+ when 0.0..3.9
64
+ 'Low'
65
+ when 4.0..6.9
66
+ 'Medium'
67
+ when 7.0..10.0
68
+ 'High'
69
+ else
70
+ raise ArgumentError, "invalid CVSS v2 score: #{cvss_v2}"
71
+ end
72
+ elsif cvss_v3
73
+ case cvss_v3
74
+ when 0.0
75
+ 'None'
76
+ when 0.1..3.9
77
+ 'Low'
78
+ when 4.0..6.9
79
+ 'Medium'
80
+ when 7.0..8.9
81
+ 'High'
82
+ when 9.0..10
83
+ 'Critical'
84
+ else
85
+ raise ArgumentError, "invalid CVSS v3 score: #{cvss_v3}"
86
+ end
87
+ end
88
+ end
47
89
  end
48
90
  end
@@ -8,8 +8,6 @@ module RedmineAudit
8
8
  URL = 'http://www.redmine.org/projects/redmine/wiki/Security_Advisories'
9
9
  TABLE_XPATH = '//*[@id="content"]/div[2]/table'
10
10
 
11
- attr_reader :vulnerabilities
12
-
13
11
  # Get unfixed advisories against specified Redmine version.
14
12
  #
15
13
  # @param [String] version
@@ -24,7 +22,7 @@ module RedmineAudit
24
22
  doc = Nokogiri::HTML(html)
25
23
  doc.xpath(TABLE_XPATH).xpath('tr')[1..-1].each do |tr|
26
24
  if res = parse_tds(tr.xpath('td'))
27
- @known_advisories << Advisory.new(*res)
25
+ @known_advisories << Advisory.new(*res, nil, nil, nil)
28
26
  end
29
27
  end
30
28
  end
@@ -0,0 +1,62 @@
1
+ require 'open-uri'
2
+ require 'yaml'
3
+ # TODO: use Redmine's Redmine::Plugin
4
+ begin
5
+ require 'redmine/plugin'
6
+ rescue LoadError
7
+ end
8
+ require 'redmine_audit/advisory'
9
+
10
+ module RedmineAudit
11
+ # Redmine plugin advisory database
12
+ class PluginDatabase
13
+ URL = 'https://raw.githubusercontent.com/sho-h/redmine_audit/master/data/plugin_advisories.yml'
14
+
15
+ # Get unfixed plugin advisories against specified Redmine version.
16
+ #
17
+ # @return [[Redmine::Advisory]]
18
+ # The array of plugin's Redmine::Advisory unfixed.
19
+ def advisories
20
+ if @known_advisories.nil?
21
+ @known_advisories = {}
22
+ YAML.load(fetch_advisory_data).each do |plugin_id, advisories|
23
+ @known_advisories[plugin_id] ||= []
24
+ advisories.each do |cve_id, attrs|
25
+ unaffected_vers = (attrs['unaffected_versions'] || []).map { |ver|
26
+ Gem::Requirement.new(ver)
27
+ }
28
+ patched_vers = (attrs['patched_versions'] || []).map { |ver|
29
+ Gem::Requirement.new(ver)
30
+ }
31
+ args = [
32
+ nil, attrs['title'], [attrs['url']],
33
+ unaffected_vers, patched_vers,
34
+ cve_id, attrs['cvss_v2'], attrs['cvss_v3'],
35
+ ]
36
+ @known_advisories[plugin_id] << Advisory.new(*args)
37
+ end
38
+ end
39
+ end
40
+
41
+ unfixed_advisories = {}
42
+ Redmine::Plugin.all.each do |plugin|
43
+ advisories = @known_advisories[plugin.id]
44
+ next if advisories.nil? || advisories.empty?
45
+
46
+ advisories.each do |advisory|
47
+ if advisory.vulnerable?(Gem::Version.new(plugin.version))
48
+ unfixed_advisories[plugin] ||= []
49
+ unfixed_advisories[plugin].push(advisory)
50
+ end
51
+ end
52
+ end
53
+ return unfixed_advisories
54
+ end
55
+
56
+ private
57
+
58
+ def fetch_advisory_data(url = URL)
59
+ open(url).read
60
+ end
61
+ end
62
+ end
@@ -1,3 +1,3 @@
1
1
  module RedmineAudit
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -1,32 +1,70 @@
1
1
  require 'redmine/version'
2
2
  require 'redmine_audit/advisory'
3
3
  require 'redmine_audit/database'
4
+ require 'redmine_audit/plugin_database'
5
+ require 'bundler/audit/scanner'
6
+ require 'ruby_audit/database'
7
+ require 'ruby_audit/scanner'
4
8
 
5
9
  desc <<-END_DESC
6
10
  Check redmine vulnerabilities.
7
11
 
8
12
  Available options:
9
- * users => comma separated list of user/group ids who should be notified
13
+ * users => comma separated list of user/group ids who should be notified
14
+ * dont_check_ruby_and_gems => don't bundler-audit and ruby_audit if '1' specified
10
15
 
11
16
  Example:
12
- rake redmine:bundle_audit users="1,23, 56" RAILS_ENV="production"
17
+ rake redmine:audit users="1,23, 56" RAILS_ENV="production"
13
18
  END_DESC
14
19
 
15
20
  namespace :redmine do
16
- # Avoid to define same task twice.
17
- # TODO: stop load twice this .rake file.
18
- if !Rake::Task.task_defined?(:audit)
19
- task audit: :environment do
20
- # TODO: More better if requires mailer automatically.
21
- require_dependency 'mailer'
22
- require_relative '../../app/models/mailer.rb'
23
-
24
- redmine_ver = Redmine::VERSION
25
- advisories = RedmineAudit::Database.new.advisories(redmine_ver.to_s)
26
- if advisories.length > 0
27
- users = (ENV['users'] || '').split(',').each(&:strip!)
21
+ task audit: :environment do
22
+ user_ids = (ENV['users'] || '').split(',').each(&:strip!)
23
+ if user_ids.empty?
24
+ raise 'need to specify environment variable: users'
25
+ end
26
+
27
+ users = User.active.where(admin: true, id: user_ids).to_a
28
+ if users.empty?
29
+ raise ActiveRecord::RecordNotFound.new("Couldn't find user specified: #{user_ids.inspect}")
30
+ end
31
+
32
+ # TODO: More better if requires mailer automatically.
33
+ require_dependency 'mailer'
34
+ require_relative '../../app/models/mailer.rb'
35
+ Rake::Task['redmine:audit:redmine'].invoke(users)
36
+ unless ENV['dont_check_ruby_and_gems'] == '1'
37
+ Rake::Task['redmine:audit:ruby_and_gems'].invoke(users)
38
+ end
39
+ end
40
+
41
+ namespace :audit do
42
+ task :redmine, [:users] do |t, args|
43
+ redmine_ver = Redmine::VERSION.to_s
44
+ advisories = RedmineAudit::Database.new.advisories(redmine_ver)
45
+ plugin_advisories = RedmineAudit::PluginDatabase.new.advisories
46
+ if advisories.length > 0 || plugin_advisories.length > 0
47
+ Mailer.with_synched_deliveries do
48
+ Mailer.unfixed_redmine_advisories_found(redmine_ver, advisories, plugin_advisories, args.users).deliver
49
+ end
50
+ end
51
+ end
52
+
53
+ # Update ruby-advisory-db.
54
+ task :update_ruby_advisory_db do
55
+ begin
56
+ Bundler::Audit::Database.update!(quiet: true)
57
+ rescue => e
58
+ warn "failed to update ruby-advisory-db: #{e.message}"
59
+ end
60
+ end
61
+
62
+ task :ruby_and_gems, [:users] => :update_ruby_advisory_db do |t, args|
63
+ ruby_result = RubyAudit::Scanner.new.scan
64
+ gems_result = Bundler::Audit::Scanner.new.scan
65
+ if ruby_result.count > 0 || gems_result.count > 0
28
66
  Mailer.with_synched_deliveries do
29
- Mailer.unfixed_advisories_found(redmine_ver, advisories, users).deliver
67
+ Mailer.unfixed_ruby_and_gems_advisories_found(ruby_result, gems_result, args.users).deliver
30
68
  end
31
69
  end
32
70
  end
@@ -22,9 +22,12 @@ Gem::Specification.new do |spec|
22
22
  spec.require_paths = ['lib']
23
23
 
24
24
  spec.add_runtime_dependency 'nokogiri'
25
+ spec.add_runtime_dependency 'bundler-audit'
26
+ spec.add_runtime_dependency 'ruby_audit'
25
27
 
26
28
  spec.add_development_dependency 'bundler', '~> 1.14'
27
29
  spec.add_development_dependency 'rake', '~> 10.0'
28
30
  spec.add_development_dependency 'test-unit'
29
31
  spec.add_development_dependency 'test-unit-rr'
32
+ spec.add_development_dependency 'travis'
30
33
  end
@@ -0,0 +1,42 @@
1
+ require 'open-uri'
2
+ require 'nokogiri'
3
+ require 'yaml'
4
+
5
+ URL_BASE = 'http://www.redmine.org/'
6
+ LIST_PATH = File.join(__dir__, '../data/plugins.yml')
7
+
8
+ plugins = {}
9
+ page = 1
10
+ loop do
11
+ puts "chacking page:#{page}..."
12
+ doc = Nokogiri::HTML(open(URL_BASE + "plugins?page=#{page}").read)
13
+ doc.xpath('//a[@class="plugin"]').each do |a|
14
+ path = a.attributes["href"].value
15
+ name = path.split('/').last
16
+ plugins[name] = {}
17
+
18
+ sleep 60
19
+ puts "chacking page:#{page}:#{name}..."
20
+
21
+ detail_url = File.join(URL_BASE, path)
22
+ detail_doc = Nokogiri::HTML(open(detail_url).read)
23
+ {website: 2, repository: 3}.each do |key, index|
24
+ xpath = "//*[@id=\"content\"]/div[2]/table/tr[#{index}]/td/a"
25
+ detail_doc.xpath(xpath).each do |da|
26
+ plugins[name][key] = da.attributes["href"].value
27
+ end
28
+ end
29
+ end
30
+ if doc.xpath('//a[@class="next"]').count > 0
31
+ page += 1
32
+ else
33
+ break
34
+ end
35
+
36
+ File.open(LIST_PATH, 'w') do |f|
37
+ f.write(plugins.to_yaml)
38
+ end
39
+
40
+ sleep 60
41
+ end
42
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redmine_audit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sho Hashimoto
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-06-17 00:00:00.000000000 Z
11
+ date: 2018-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler-audit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ruby_audit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: bundler
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +108,20 @@ dependencies:
80
108
  - - ">="
81
109
  - !ruby/object:Gem::Version
82
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: travis
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
83
125
  description: Redmine plugin for checking Redmine's own vulnerabilities
84
126
  email:
85
127
  - sho.hsmt@gmail.com
@@ -88,30 +130,38 @@ extensions: []
88
130
  extra_rdoc_files: []
89
131
  files:
90
132
  - ".gitignore"
133
+ - ".travis.yml"
91
134
  - LICENSE
92
135
  - README.md
93
136
  - Rakefile
94
137
  - app/controllers/.keep
95
138
  - app/helpers/.keep
139
+ - app/helpers/redmine_audit_helper.rb
96
140
  - app/models/.keep
97
141
  - app/models/mailer.rb
98
142
  - app/views/.keep
99
- - app/views/mailer/unfixed_advisories_found.html.erb
100
- - app/views/mailer/unfixed_advisories_found.text.erb
143
+ - app/views/mailer/unfixed_redmine_advisories_found.html.erb
144
+ - app/views/mailer/unfixed_redmine_advisories_found.text.erb
145
+ - app/views/mailer/unfixed_ruby_and_gems_advisories_found.html.erb
146
+ - app/views/mailer/unfixed_ruby_and_gems_advisories_found.text.erb
101
147
  - bin/console
102
148
  - bin/setup
103
149
  - config/locales/en.yml
104
150
  - config/locales/ja.yml
105
151
  - config/routes.rb
152
+ - data/plugin_advisories.yml
153
+ - data/plugins.yml
106
154
  - db/migrate/.keep
107
155
  - init.rb
108
156
  - lib/redmine_audit.rb
109
157
  - lib/redmine_audit/advisory.rb
110
158
  - lib/redmine_audit/database.rb
159
+ - lib/redmine_audit/plugin_database.rb
111
160
  - lib/redmine_audit/version.rb
112
161
  - lib/tasks/.keep
113
162
  - lib/tasks/redmine_audit.rake
114
163
  - redmine_audit.gemspec
164
+ - scripts/create_plugin_list
115
165
  homepage: https://github.com/sho-h/redmine_audit
116
166
  licenses:
117
167
  - MIT
@@ -132,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
182
  version: '0'
133
183
  requirements: []
134
184
  rubyforge_project:
135
- rubygems_version: 2.5.2
185
+ rubygems_version: 2.5.2.3
136
186
  signing_key:
137
187
  specification_version: 4
138
188
  summary: Redmine plugin for checking Redmine's own vulnerabilities
@@ -1,27 +0,0 @@
1
- <div><%= l(:mail_summary_advisories_found) %></div>
2
-
3
- <%-
4
- @advisories.each do |advisory|
5
- if advisory.external_references && !advisory.external_references.empty?
6
- ext_refs = advisory.external_references
7
- else
8
- ext_refs = 'none'
9
- end
10
- solution = advisory.fixed_versions.join(', ')
11
- -%>
12
-
13
- <div>
14
- <ul style="list-style:none;">
15
- <li>Name: Redmine</li>
16
- <li>Version: <%= @redmine_version %></li>
17
- <li>Severity: <%= advisory.severity %></li>
18
- <li>URL: <%= ext_refs %></li>
19
- <li>Detail: <%= advisory.details %></li>
20
- <li>Solution: upgrade to <%= solution %></li>
21
- </ul>
22
- </div>
23
- <%- end -%>
24
-
25
- <div>
26
- <%= l(:mail_detail_link_head_advisories_found) %> <%= link_to 'redmine.org', RedmineAudit::Database::URL %> <%= l(:mail_detail_link_tail_advisories_found) %>
27
- </div>