pluginscan 0.9.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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.gitlab-ci.yml +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +46 -0
- data/.rubocop_todo.yml +36 -0
- data/CHANGELOG.md +89 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +90 -0
- data/README.md +56 -0
- data/Rakefile +2 -0
- data/TODO.md +8 -0
- data/bin/pluginscan +53 -0
- data/lib/file_creator.rb +18 -0
- data/lib/pluginscan.rb +69 -0
- data/lib/pluginscan/error.rb +9 -0
- data/lib/pluginscan/error_printer.rb +17 -0
- data/lib/pluginscan/file_finder.rb +42 -0
- data/lib/pluginscan/printer.rb +14 -0
- data/lib/pluginscan/reports/cloc_report.rb +27 -0
- data/lib/pluginscan/reports/cloc_report/cloc.rb +21 -0
- data/lib/pluginscan/reports/cloc_report/cloc_printer.rb +42 -0
- data/lib/pluginscan/reports/cloc_report/cloc_scanner.rb +41 -0
- data/lib/pluginscan/reports/cloc_report/system_cloc.rb +33 -0
- data/lib/pluginscan/reports/issues_report.rb +24 -0
- data/lib/pluginscan/reports/issues_report/error_list_printer.rb +99 -0
- data/lib/pluginscan/reports/issues_report/issue_checks.rb +382 -0
- data/lib/pluginscan/reports/issues_report/issue_checks/check.rb +55 -0
- data/lib/pluginscan/reports/issues_report/issue_checks/comment_checker.rb +13 -0
- data/lib/pluginscan/reports/issues_report/issue_checks/function_check.rb +32 -0
- data/lib/pluginscan/reports/issues_report/issue_checks/variable_check.rb +14 -0
- data/lib/pluginscan/reports/issues_report/issue_checks/variable_safety_checker.rb +112 -0
- data/lib/pluginscan/reports/issues_report/issues_models/check_findings.rb +29 -0
- data/lib/pluginscan/reports/issues_report/issues_models/issues.rb +31 -0
- data/lib/pluginscan/reports/issues_report/issues_printer.rb +34 -0
- data/lib/pluginscan/reports/issues_report/issues_printer/check_findings_printer.rb +37 -0
- data/lib/pluginscan/reports/issues_report/issues_printer/file_issues_printer.rb +36 -0
- data/lib/pluginscan/reports/issues_report/issues_printer/finding_printer.rb +38 -0
- data/lib/pluginscan/reports/issues_report/issues_printer_factory.rb +19 -0
- data/lib/pluginscan/reports/issues_report/issues_scanner.rb +49 -0
- data/lib/pluginscan/reports/issues_report/issues_scanner/file_issues_scanner.rb +39 -0
- data/lib/pluginscan/reports/issues_report/issues_scanner/line_issues_scanner.rb +15 -0
- data/lib/pluginscan/reports/issues_report/issues_scanner/utf8_checker.rb +14 -0
- data/lib/pluginscan/reports/sloccount_report.rb +26 -0
- data/lib/pluginscan/reports/sloccount_report/sloccount.rb +19 -0
- data/lib/pluginscan/reports/sloccount_report/sloccount_printer.rb +22 -0
- data/lib/pluginscan/reports/sloccount_report/sloccount_scanner.rb +86 -0
- data/lib/pluginscan/reports/vulnerability_report.rb +28 -0
- data/lib/pluginscan/reports/vulnerability_report/advisories_api.rb +23 -0
- data/lib/pluginscan/reports/vulnerability_report/vulnerabilities_printer.rb +55 -0
- data/lib/pluginscan/reports/vulnerability_report/vulnerability_scanner.rb +17 -0
- data/lib/pluginscan/reports/vulnerability_report/wp_vuln_db_api.rb +77 -0
- data/lib/pluginscan/version.rb +3 -0
- data/pluginscan.gemspec +31 -0
- data/spec/acceptance/cloc_spec.rb +54 -0
- data/spec/acceptance/create_error_list_file_spec.rb +29 -0
- data/spec/acceptance/issues_spec.rb +197 -0
- data/spec/acceptance/pluginscan_spec.rb +18 -0
- data/spec/acceptance/sloccount_spec.rb +39 -0
- data/spec/acceptance/vulnerabilities_spec.rb +57 -0
- data/spec/acceptance_spec_helper.rb +10 -0
- data/spec/checks_examples_spec.rb +352 -0
- data/spec/file_creator_spec.rb +51 -0
- data/spec/pluginscan/cloc_scanner/cloc_scanner_spec.rb +64 -0
- data/spec/pluginscan/cloc_scanner/cloc_spec.rb +30 -0
- data/spec/pluginscan/file_finder_spec.rb +91 -0
- data/spec/pluginscan/issues_scanner/check_findings_spec.rb +22 -0
- data/spec/pluginscan/issues_scanner/error_list_printer_ignores_spec.rb +35 -0
- data/spec/pluginscan/issues_scanner/error_list_printer_spec.rb +42 -0
- data/spec/pluginscan/issues_scanner/file_issues_scanner_spec.rb +25 -0
- data/spec/pluginscan/issues_scanner/issues_printer_factory_spec.rb +9 -0
- data/spec/pluginscan/issues_scanner/issues_spec.rb +55 -0
- data/spec/pluginscan/issues_scanner/variable_check_spec.rb +13 -0
- data/spec/pluginscan/issues_scanner/variable_safety_checker_spec.rb +81 -0
- data/spec/pluginscan/issues_scanner_spec.rb +21 -0
- data/spec/pluginscan/sloccount_scanner/sloccount_scanner_spec.rb +95 -0
- data/spec/pluginscan/sloccount_scanner/sloccount_spec.rb +72 -0
- data/spec/pluginscan/vulnerability_scanner_spec.rb +96 -0
- data/spec/process_spec_helper.rb +6 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/support/acceptance_helpers.rb +68 -0
- data/spec/support/file_helpers.rb +35 -0
- data/spec/support/heredoc_helper.rb +7 -0
- data/spec/support/process_helpers.rb +25 -0
- data/spec/support/shared_examples_for_issue_checks.rb +31 -0
- data/spec/support/vcr_helper.rb +6 -0
- data/vcr_cassettes/wpvulndb/relevanssi.yml +78 -0
- metadata +342 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Pluginscan
|
|
2
|
+
# Responsible for scanning one line of a file for issues of a certain type
|
|
3
|
+
class LineIssuesScanner
|
|
4
|
+
def initialize(check)
|
|
5
|
+
@check = check
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def scan(line, lineno)
|
|
9
|
+
@check.run(line).map do |match|
|
|
10
|
+
ignored = @check.ignore?(match, line)
|
|
11
|
+
Finding.new(lineno, line, match, ignored)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Pluginscan
|
|
2
|
+
# Responsible for checking a file for utf-8 validity
|
|
3
|
+
class UTF8Checker
|
|
4
|
+
# TODO: This returns nil if the file was valid. Returning nil is bad.
|
|
5
|
+
def check(file_contents)
|
|
6
|
+
# Check file contents are valid UTF-8
|
|
7
|
+
file_contents.force_encoding('utf-8')
|
|
8
|
+
return if file_contents.valid_encoding?
|
|
9
|
+
CheckFindings.new(
|
|
10
|
+
Check.new(name: 'Encoding', message: 'invalid UTF-8')
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'pluginscan/reports/sloccount_report/sloccount'
|
|
2
|
+
require 'pluginscan/reports/sloccount_report/sloccount_scanner'
|
|
3
|
+
require 'pluginscan/reports/sloccount_report/sloccount_printer'
|
|
4
|
+
require 'pluginscan/error_printer'
|
|
5
|
+
|
|
6
|
+
module Pluginscan
|
|
7
|
+
module Reports
|
|
8
|
+
module SLOCCountReport
|
|
9
|
+
def self.print(plugin_directory, printer, error_printer)
|
|
10
|
+
sloccount = SLOCCountScanner.new.scan(plugin_directory)
|
|
11
|
+
printer.print(sloccount)
|
|
12
|
+
|
|
13
|
+
rescue SLOCCountScanner::Unavailable
|
|
14
|
+
error_printer.print("The 'sloccount' command is unavailable. Is it installed and in your path?")
|
|
15
|
+
rescue SLOCCountScanner::Exception => e
|
|
16
|
+
error_printer.print("An error occurred while calculating the SLOCCount:\n #{e.message}")
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
# We don't want an error with CLOC to interrupt the rest of pluginscan
|
|
19
|
+
# TODO: not sure this is sensible
|
|
20
|
+
error_printer.print("An error occurred while calculating the SLOCCount:\n #{e.message}")
|
|
21
|
+
else
|
|
22
|
+
return true
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Responsible for accessing specific data in the output of the `sloccount` command
|
|
2
|
+
class SLOCCount
|
|
3
|
+
def initialize(sloccount_output)
|
|
4
|
+
@sloccount_output = sloccount_output
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def total
|
|
8
|
+
total_regex = /^Total Physical Source Lines of Code.*= ([\d\,]+)/
|
|
9
|
+
# Numbers in sloccount are displayed with commas e.g. 1,791
|
|
10
|
+
@sloccount_output.match(total_regex)[1].delete(',').to_i
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# TODO: should be ZeroSLOCCount? More descriptive, but less obvious it's null object pattern
|
|
15
|
+
class NullSLOCCount
|
|
16
|
+
def total
|
|
17
|
+
0
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'pluginscan/printer'
|
|
2
|
+
|
|
3
|
+
module Pluginscan
|
|
4
|
+
class SLOCCountPrinter < Printer
|
|
5
|
+
def print(sloccount)
|
|
6
|
+
print_headline
|
|
7
|
+
print_results(sloccount)
|
|
8
|
+
print_blank_line
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def print_headline
|
|
14
|
+
@output.puts "Total sloccount (from David A. Wheeler's 'SLOCCount' tool):".color(:blue)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def print_results(sloccount)
|
|
18
|
+
total = sloccount.total.to_s.color(:red)
|
|
19
|
+
@output.puts " #{total} source lines of code"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
# Responsible for running the `sloccount` system command and handling any resulting errors
|
|
4
|
+
# The SLOCCount project is at http://www.dwheeler.com/sloccount/ and can be installed on OSX using homebrew:
|
|
5
|
+
# brew install sloccount
|
|
6
|
+
class SLOCCountScanner
|
|
7
|
+
class Exception < StandardError; end
|
|
8
|
+
class ArgumentError < RuntimeError; end
|
|
9
|
+
class Unavailable < RuntimeError; end
|
|
10
|
+
class NoInput < RuntimeError; end
|
|
11
|
+
class NoDirectory < RuntimeError; end
|
|
12
|
+
|
|
13
|
+
def initialize(system_sloccount = SystemSLOCCount.instance)
|
|
14
|
+
@system_sloccount = system_sloccount
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def scan(directory)
|
|
18
|
+
fail ArgumentError, "directory must must be a string (or quack like a string)" unless directory.respond_to?(:to_str)
|
|
19
|
+
fail Unavailable, "The 'sloccount' command is unavailable. Is it installed and in your path?" unless @system_sloccount.available?
|
|
20
|
+
fail NoDirectory, "No such directory: '#{directory}'" unless Dir.exist?(directory)
|
|
21
|
+
|
|
22
|
+
result, status = @system_sloccount.call(directory)
|
|
23
|
+
|
|
24
|
+
if status.success?
|
|
25
|
+
SLOCCount.new(result)
|
|
26
|
+
else
|
|
27
|
+
check_for_errors(result) { NullSLOCCount.new }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def check_for_errors(result)
|
|
34
|
+
case result
|
|
35
|
+
when /SLOC total is zero, no further analysis performed/
|
|
36
|
+
yield
|
|
37
|
+
|
|
38
|
+
when /Error: You must provide a directory or directories of source code/
|
|
39
|
+
# Because of the check above, it shouldn't be possible to get here
|
|
40
|
+
raise NoInput, "sloccount requires a directory or directories of source code"
|
|
41
|
+
else
|
|
42
|
+
raise Exception, "sloccount raised an error we didn't recognise. Here's the output:\n#{result}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Responsible for isolating the system calls to run SLOCCount
|
|
47
|
+
class SystemSLOCCount
|
|
48
|
+
require 'singleton'
|
|
49
|
+
include Singleton
|
|
50
|
+
|
|
51
|
+
COMMAND_NAME = "sloccount".freeze
|
|
52
|
+
|
|
53
|
+
def available?
|
|
54
|
+
_result, status = system_which_sloccount
|
|
55
|
+
status.success?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def call(directory)
|
|
59
|
+
# DANGER!!! calling system command
|
|
60
|
+
# also: danger! not covered by specs
|
|
61
|
+
|
|
62
|
+
# Should be safe from injection because of the `Dir.exist?` check.
|
|
63
|
+
Open3.capture2("#{COMMAND_NAME} #{directory} 2> #{error_log_name}")
|
|
64
|
+
|
|
65
|
+
ensure
|
|
66
|
+
delete_empty_error_log
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def system_which_sloccount
|
|
72
|
+
# DANGER!!! calling system command
|
|
73
|
+
# also: danger! not covered by specs
|
|
74
|
+
|
|
75
|
+
Open3.capture2("which #{COMMAND_NAME}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def error_log_name
|
|
79
|
+
"#{COMMAND_NAME}_error.log"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def delete_empty_error_log
|
|
83
|
+
File.delete(error_log_name) if File.zero?(error_log_name)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'pluginscan/reports/vulnerability_report/advisories_api'
|
|
2
|
+
require 'pluginscan/reports/vulnerability_report/wp_vuln_db_api'
|
|
3
|
+
require 'pluginscan/reports/vulnerability_report/vulnerability_scanner'
|
|
4
|
+
require 'pluginscan/reports/vulnerability_report/vulnerabilities_printer'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module Pluginscan
|
|
8
|
+
module Reports
|
|
9
|
+
module VulnerabilityReport
|
|
10
|
+
def self.print(plugin_directory, printer, error_printer)
|
|
11
|
+
plugin_slug = get_plugin_slug(plugin_directory)
|
|
12
|
+
advisories = VulnerabilityScanner.new.scan(plugin_slug)
|
|
13
|
+
|
|
14
|
+
printer.print(advisories, plugin_slug)
|
|
15
|
+
true
|
|
16
|
+
rescue WPVulnDB::APIError => e
|
|
17
|
+
error_printer.print(e.message)
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.get_plugin_slug(plugin_directory)
|
|
22
|
+
# Expanding the path handles '.' and '..' etc.
|
|
23
|
+
full_path = File.expand_path(plugin_directory)
|
|
24
|
+
full_path.split('/').last
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'httparty'
|
|
2
|
+
|
|
3
|
+
module Pluginscan
|
|
4
|
+
# Responsible for calling an api endpoint
|
|
5
|
+
# and re-raising ruby errors with more information
|
|
6
|
+
class AdvisoriesAPI
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
class ConnectionError < Error; end
|
|
9
|
+
|
|
10
|
+
def initialize(api_name:, timeout:)
|
|
11
|
+
@api_name = api_name
|
|
12
|
+
@timeout = timeout
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(uri)
|
|
16
|
+
HTTParty.get(uri, timeout: @timeout)
|
|
17
|
+
rescue SocketError
|
|
18
|
+
raise(ConnectionError, "Couldn't connect to #{@api_name} (SocketError)")
|
|
19
|
+
rescue Net::OpenTimeout
|
|
20
|
+
raise(ConnectionError, "Connection to #{@api_name} timed out after #{@timeout} seconds")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Pluginscan
|
|
2
|
+
class VulnerabilitiesPrinter < Printer
|
|
3
|
+
def print(advisories, plugin_slug)
|
|
4
|
+
raise ArgumentError, "Can't print a nil list of advisories" if advisories.nil?
|
|
5
|
+
|
|
6
|
+
print_headline(advisories, plugin_slug)
|
|
7
|
+
|
|
8
|
+
advisories.reverse.each do |advisory|
|
|
9
|
+
print_advisory(advisory)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
print_blank_line
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private def print_headline(advisories, plugin_slug)
|
|
16
|
+
if advisories.any?
|
|
17
|
+
@output.puts "#{advisories.count} advisories were found for '#{plugin_slug}':".color(:blue)
|
|
18
|
+
else
|
|
19
|
+
@output.puts "No advisories were found for '#{plugin_slug}'".color(:blue)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private def print_advisory(advisory)
|
|
24
|
+
printer = VulnerabilityPrinter.new(@output)
|
|
25
|
+
printer.print(advisory)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class VulnerabilityPrinter < Printer
|
|
30
|
+
def print(advisory)
|
|
31
|
+
title = highlight_version_number(advisory.title)
|
|
32
|
+
date = format_date(advisory.date)
|
|
33
|
+
fixed = fixed_data(advisory.fixed_in)
|
|
34
|
+
@output.puts " #{date} #{title} #{fixed}"
|
|
35
|
+
@output.puts " #{advisory.url}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private def highlight_version_number(title)
|
|
39
|
+
version_number_regex = '([\<\>\=]+\ )?[\d\.]+'
|
|
40
|
+
title.gsub(
|
|
41
|
+
/(?<version>#{version_number_regex})/,
|
|
42
|
+
'\k<version>'.color(:yellow)
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private def format_date(date)
|
|
47
|
+
date.strftime('%Y-%m-%d').color(:green)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private def fixed_data(fixed_version)
|
|
51
|
+
return "(no fixed version!)".color(:red) if fixed_version.nil?
|
|
52
|
+
"(fixed in #{fixed_version})".color(:red)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Pluginscan
|
|
2
|
+
# Responsible for calling out to an API to see if any advisories
|
|
3
|
+
# have been published about this plugin
|
|
4
|
+
class VulnerabilityScanner
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
def initialize(advisories_api = WPVulnDB::API.new, response_handler = WPVulnDB::APIResponseHandler.new)
|
|
8
|
+
@advisories_api = advisories_api
|
|
9
|
+
@response_handler = response_handler
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def scan(plugin_slug)
|
|
13
|
+
response = @advisories_api.get_plugin_advisories(plugin_slug)
|
|
14
|
+
@response_handler.call(response, plugin_slug)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module Pluginscan
|
|
2
|
+
# Responsible for calling the WPVulnDB API
|
|
3
|
+
module WPVulnDB
|
|
4
|
+
class APIError < StandardError; end
|
|
5
|
+
class AccessDeniedError < APIError; end
|
|
6
|
+
class UnexpectedJSONError < APIError; end
|
|
7
|
+
|
|
8
|
+
class API
|
|
9
|
+
def initialize
|
|
10
|
+
@api = AdvisoriesAPI.new(api_name: 'wpvulndb', timeout: 10)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get_plugin_advisories(plugin_slug)
|
|
14
|
+
@api.get(uri(plugin_slug))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def uri(plugin_slug)
|
|
20
|
+
"#{base_uri}/plugins/#{plugin_slug}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def base_uri
|
|
24
|
+
'https://wpvulndb.com/api/v2'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class APIResponseHandler
|
|
29
|
+
def call(response, plugin_slug)
|
|
30
|
+
case response.code
|
|
31
|
+
when 200
|
|
32
|
+
DataMapper.new.call(response.parsed_response, plugin_slug)
|
|
33
|
+
when 404
|
|
34
|
+
[]
|
|
35
|
+
when 403
|
|
36
|
+
raise(AccessDeniedError, "We got blocked by wpvulndb for suspicious activity :( Contact team@wpvulndb.com")
|
|
37
|
+
else
|
|
38
|
+
raise(APIError, "Something went wrong when calling wpvulndb - got a #{response.code} code: '#{response.body[0..50]}'")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class DataMapper
|
|
44
|
+
def call(response_data, plugin_slug)
|
|
45
|
+
plugin_data = response_data.fetch(plugin_slug) do
|
|
46
|
+
raise(UnexpectedJSONError, "Couldn't find data for '#{plugin_slug}' in api response")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
vulns = plugin_data.fetch('vulnerabilities') do
|
|
50
|
+
raise(UnexpectedJSONError, "Couldn't find a list of vulnerabilities")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
vulns.map{ |v| Advisory.new(v) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class Advisory
|
|
58
|
+
attr_reader :title
|
|
59
|
+
attr_reader :fixed_in
|
|
60
|
+
|
|
61
|
+
def initialize(data)
|
|
62
|
+
@id = data.fetch('id')
|
|
63
|
+
@title = data.fetch('title')
|
|
64
|
+
@created_at = data.fetch('created_at')
|
|
65
|
+
@fixed_in = data.fetch('fixed_in')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def date
|
|
69
|
+
Date.parse(@created_at)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def url
|
|
73
|
+
"https://wpvulndb.com/vulnerabilities/#{@id}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/pluginscan.gemspec
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
|
2
|
+
require 'pluginscan/version'
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |spec|
|
|
5
|
+
spec.name = "pluginscan"
|
|
6
|
+
spec.version = Pluginscan::VERSION
|
|
7
|
+
spec.authors = ["dxw"]
|
|
8
|
+
spec.email = ["security@dxw.com"]
|
|
9
|
+
spec.homepage = "https://twinkie.dxw.net/dxw/pluginscan"
|
|
10
|
+
spec.description = %q(Scans WordPress plugins for potential issues and vulnerabilities)
|
|
11
|
+
spec.summary = %q(Does stuff)
|
|
12
|
+
|
|
13
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
|
14
|
+
spec.executables = ['pluginscan']
|
|
15
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
16
|
+
spec.require_paths = ["lib"]
|
|
17
|
+
|
|
18
|
+
spec.add_dependency "rainbow", "~> 2.0"
|
|
19
|
+
spec.add_dependency "httparty", "< 1"
|
|
20
|
+
|
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
|
22
|
+
spec.add_development_dependency "rspec", "~> 3.4.0", '>= 3.4.0'
|
|
23
|
+
spec.add_development_dependency "webmock", "< 3"
|
|
24
|
+
spec.add_development_dependency "vcr", "< 4"
|
|
25
|
+
spec.add_development_dependency "simplecov", "< 1"
|
|
26
|
+
spec.add_development_dependency "rubocop", "< 1"
|
|
27
|
+
spec.add_development_dependency "fuubar", "~> 2"
|
|
28
|
+
spec.add_development_dependency "pry", "~> 0"
|
|
29
|
+
spec.add_development_dependency "rake", ">= 10.0.0"
|
|
30
|
+
spec.add_development_dependency "geminabox-release", "~> 0.2", ">= 0.2.0"
|
|
31
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'acceptance_spec_helper'
|
|
2
|
+
require 'support/heredoc_helper'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Pluginscan::Scanner do
|
|
5
|
+
before do
|
|
6
|
+
# these are slow, so don't run them if we don't have to
|
|
7
|
+
stub_cloc
|
|
8
|
+
stub_sloccount
|
|
9
|
+
stub_vuln_check
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe '.scan' do
|
|
13
|
+
let(:output) { StringIO.new }
|
|
14
|
+
|
|
15
|
+
describe "CLOC Report", type: [:file, :process] do
|
|
16
|
+
subject(:scanner) { Pluginscan::Scanner.new(sloccount: false, output: output) }
|
|
17
|
+
before(:all) { setup_tempdir 'tmp' }
|
|
18
|
+
|
|
19
|
+
it "reports on sloc by language" do
|
|
20
|
+
LanguageCount = Struct.new :language, :sloc, :file_count
|
|
21
|
+
|
|
22
|
+
cloc_output = <<-EOS.heredoc_unindent
|
|
23
|
+
files,language,blank,comment,code,"github.com/AlDanial/cloc v 1.70 T=0.29 s (284.9 files/s, 65539.8 lines/s)"
|
|
24
|
+
20,PHP,4584,0,6628
|
|
25
|
+
EOS
|
|
26
|
+
|
|
27
|
+
stub_cloc(result: cloc_output)
|
|
28
|
+
|
|
29
|
+
php = coloured_cyan('PHP')
|
|
30
|
+
count = coloured_red('6628')
|
|
31
|
+
scanner.scan 'tmp'
|
|
32
|
+
expect(output.string).to match(/#{php}\s*#{count} lines across 20 files/)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "displays a message when there was no code" do
|
|
36
|
+
stub_cloc(result: "")
|
|
37
|
+
scanner.scan 'tmp'
|
|
38
|
+
expect(output.string).to match(/CLOC didn't find any code/)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "displays a message when cloc was unavailable (doesn't error out)" do
|
|
42
|
+
stub_cloc(which_result: which_failure)
|
|
43
|
+
scanner.scan 'tmp'
|
|
44
|
+
expect(output.string).to match(/The 'cloc' command is unavailable/)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "displays a message when cloc threw an error (doesn't error out)" do
|
|
48
|
+
stub_cloc(result: "Some nonsense", process_status: failed_process_status)
|
|
49
|
+
scanner.scan 'tmp'
|
|
50
|
+
expect(output.string).to match(/Some nonsense/)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|