redmine_audit 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +11 -0
- data/README.md +4 -4
- data/app/helpers/redmine_audit_helper.rb +18 -0
- data/app/models/mailer.rb +31 -14
- data/app/views/mailer/unfixed_redmine_advisories_found.html.erb +54 -0
- data/app/views/mailer/unfixed_redmine_advisories_found.text.erb +43 -0
- data/app/views/mailer/unfixed_ruby_and_gems_advisories_found.html.erb +33 -0
- data/app/views/mailer/unfixed_ruby_and_gems_advisories_found.text.erb +27 -0
- data/config/locales/en.yml +3 -3
- data/config/locales/ja.yml +3 -3
- data/data/plugin_advisories.yml +19 -0
- data/data/plugins.yml +2468 -0
- data/init.rb +2 -1
- data/lib/redmine_audit/advisory.rb +49 -7
- data/lib/redmine_audit/database.rb +1 -3
- data/lib/redmine_audit/plugin_database.rb +62 -0
- data/lib/redmine_audit/version.rb +1 -1
- data/lib/tasks/redmine_audit.rake +53 -15
- data/redmine_audit.gemspec +3 -0
- data/scripts/create_plugin_list +42 -0
- metadata +55 -5
- data/app/views/mailer/unfixed_advisories_found.html.erb +0 -27
- data/app/views/mailer/unfixed_advisories_found.text.erb +0 -21
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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,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
|
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:
|
17
|
+
rake redmine:audit users="1,23, 56" RAILS_ENV="production"
|
13
18
|
END_DESC
|
14
19
|
|
15
20
|
namespace :redmine do
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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.
|
67
|
+
Mailer.unfixed_ruby_and_gems_advisories_found(ruby_result, gems_result, args.users).deliver
|
30
68
|
end
|
31
69
|
end
|
32
70
|
end
|
data/redmine_audit.gemspec
CHANGED
@@ -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.
|
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:
|
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/
|
100
|
-
- app/views/mailer/
|
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>
|