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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.gitlab-ci.yml +16 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +46 -0
  6. data/.rubocop_todo.yml +36 -0
  7. data/CHANGELOG.md +89 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.lock +90 -0
  10. data/README.md +56 -0
  11. data/Rakefile +2 -0
  12. data/TODO.md +8 -0
  13. data/bin/pluginscan +53 -0
  14. data/lib/file_creator.rb +18 -0
  15. data/lib/pluginscan.rb +69 -0
  16. data/lib/pluginscan/error.rb +9 -0
  17. data/lib/pluginscan/error_printer.rb +17 -0
  18. data/lib/pluginscan/file_finder.rb +42 -0
  19. data/lib/pluginscan/printer.rb +14 -0
  20. data/lib/pluginscan/reports/cloc_report.rb +27 -0
  21. data/lib/pluginscan/reports/cloc_report/cloc.rb +21 -0
  22. data/lib/pluginscan/reports/cloc_report/cloc_printer.rb +42 -0
  23. data/lib/pluginscan/reports/cloc_report/cloc_scanner.rb +41 -0
  24. data/lib/pluginscan/reports/cloc_report/system_cloc.rb +33 -0
  25. data/lib/pluginscan/reports/issues_report.rb +24 -0
  26. data/lib/pluginscan/reports/issues_report/error_list_printer.rb +99 -0
  27. data/lib/pluginscan/reports/issues_report/issue_checks.rb +382 -0
  28. data/lib/pluginscan/reports/issues_report/issue_checks/check.rb +55 -0
  29. data/lib/pluginscan/reports/issues_report/issue_checks/comment_checker.rb +13 -0
  30. data/lib/pluginscan/reports/issues_report/issue_checks/function_check.rb +32 -0
  31. data/lib/pluginscan/reports/issues_report/issue_checks/variable_check.rb +14 -0
  32. data/lib/pluginscan/reports/issues_report/issue_checks/variable_safety_checker.rb +112 -0
  33. data/lib/pluginscan/reports/issues_report/issues_models/check_findings.rb +29 -0
  34. data/lib/pluginscan/reports/issues_report/issues_models/issues.rb +31 -0
  35. data/lib/pluginscan/reports/issues_report/issues_printer.rb +34 -0
  36. data/lib/pluginscan/reports/issues_report/issues_printer/check_findings_printer.rb +37 -0
  37. data/lib/pluginscan/reports/issues_report/issues_printer/file_issues_printer.rb +36 -0
  38. data/lib/pluginscan/reports/issues_report/issues_printer/finding_printer.rb +38 -0
  39. data/lib/pluginscan/reports/issues_report/issues_printer_factory.rb +19 -0
  40. data/lib/pluginscan/reports/issues_report/issues_scanner.rb +49 -0
  41. data/lib/pluginscan/reports/issues_report/issues_scanner/file_issues_scanner.rb +39 -0
  42. data/lib/pluginscan/reports/issues_report/issues_scanner/line_issues_scanner.rb +15 -0
  43. data/lib/pluginscan/reports/issues_report/issues_scanner/utf8_checker.rb +14 -0
  44. data/lib/pluginscan/reports/sloccount_report.rb +26 -0
  45. data/lib/pluginscan/reports/sloccount_report/sloccount.rb +19 -0
  46. data/lib/pluginscan/reports/sloccount_report/sloccount_printer.rb +22 -0
  47. data/lib/pluginscan/reports/sloccount_report/sloccount_scanner.rb +86 -0
  48. data/lib/pluginscan/reports/vulnerability_report.rb +28 -0
  49. data/lib/pluginscan/reports/vulnerability_report/advisories_api.rb +23 -0
  50. data/lib/pluginscan/reports/vulnerability_report/vulnerabilities_printer.rb +55 -0
  51. data/lib/pluginscan/reports/vulnerability_report/vulnerability_scanner.rb +17 -0
  52. data/lib/pluginscan/reports/vulnerability_report/wp_vuln_db_api.rb +77 -0
  53. data/lib/pluginscan/version.rb +3 -0
  54. data/pluginscan.gemspec +31 -0
  55. data/spec/acceptance/cloc_spec.rb +54 -0
  56. data/spec/acceptance/create_error_list_file_spec.rb +29 -0
  57. data/spec/acceptance/issues_spec.rb +197 -0
  58. data/spec/acceptance/pluginscan_spec.rb +18 -0
  59. data/spec/acceptance/sloccount_spec.rb +39 -0
  60. data/spec/acceptance/vulnerabilities_spec.rb +57 -0
  61. data/spec/acceptance_spec_helper.rb +10 -0
  62. data/spec/checks_examples_spec.rb +352 -0
  63. data/spec/file_creator_spec.rb +51 -0
  64. data/spec/pluginscan/cloc_scanner/cloc_scanner_spec.rb +64 -0
  65. data/spec/pluginscan/cloc_scanner/cloc_spec.rb +30 -0
  66. data/spec/pluginscan/file_finder_spec.rb +91 -0
  67. data/spec/pluginscan/issues_scanner/check_findings_spec.rb +22 -0
  68. data/spec/pluginscan/issues_scanner/error_list_printer_ignores_spec.rb +35 -0
  69. data/spec/pluginscan/issues_scanner/error_list_printer_spec.rb +42 -0
  70. data/spec/pluginscan/issues_scanner/file_issues_scanner_spec.rb +25 -0
  71. data/spec/pluginscan/issues_scanner/issues_printer_factory_spec.rb +9 -0
  72. data/spec/pluginscan/issues_scanner/issues_spec.rb +55 -0
  73. data/spec/pluginscan/issues_scanner/variable_check_spec.rb +13 -0
  74. data/spec/pluginscan/issues_scanner/variable_safety_checker_spec.rb +81 -0
  75. data/spec/pluginscan/issues_scanner_spec.rb +21 -0
  76. data/spec/pluginscan/sloccount_scanner/sloccount_scanner_spec.rb +95 -0
  77. data/spec/pluginscan/sloccount_scanner/sloccount_spec.rb +72 -0
  78. data/spec/pluginscan/vulnerability_scanner_spec.rb +96 -0
  79. data/spec/process_spec_helper.rb +6 -0
  80. data/spec/spec_helper.rb +70 -0
  81. data/spec/support/acceptance_helpers.rb +68 -0
  82. data/spec/support/file_helpers.rb +35 -0
  83. data/spec/support/heredoc_helper.rb +7 -0
  84. data/spec/support/process_helpers.rb +25 -0
  85. data/spec/support/shared_examples_for_issue_checks.rb +31 -0
  86. data/spec/support/vcr_helper.rb +6 -0
  87. data/vcr_cassettes/wpvulndb/relevanssi.yml +78 -0
  88. metadata +342 -0
@@ -0,0 +1,69 @@
1
+ require 'pluginscan/reports/sloccount_report'
2
+ require 'pluginscan/reports/cloc_report'
3
+ require 'pluginscan/reports/vulnerability_report'
4
+ require 'pluginscan/reports/issues_report'
5
+ require 'pluginscan/reports/issues_report/issues_printer_factory'
6
+ require 'pluginscan/error'
7
+
8
+ module Pluginscan
9
+ class Scanner
10
+ DEFAULT_OPTIONS = {
11
+ output: $stdout,
12
+ sloccount: true,
13
+ cloc: true,
14
+ advisories: true,
15
+ issues_format: :report,
16
+ hide_ignores: false,
17
+ }.freeze
18
+
19
+ def initialize(options = {})
20
+ @options = DEFAULT_OPTIONS.merge options
21
+ end
22
+
23
+ def scan(plugin_directory)
24
+ fail Errno::ENOENT unless Dir.exist? plugin_directory
25
+
26
+ output_sloccount_report(plugin_directory) if @options[:sloccount]
27
+ output_cloc_report(plugin_directory) if @options[:cloc]
28
+
29
+ output_vulnerability_report(plugin_directory) if @options[:advisories]
30
+
31
+ output_issues_report(plugin_directory)
32
+ output_error_list_to_file(plugin_directory) if @options[:error_list_file]
33
+
34
+ true
35
+ end
36
+
37
+ private def output_sloccount_report(plugin_directory)
38
+ sloccount_printer = SLOCCountPrinter.new(@options[:output])
39
+ Reports::SLOCCountReport.print(plugin_directory, sloccount_printer, error_printer)
40
+ end
41
+
42
+ private def output_cloc_report(plugin_directory)
43
+ cloc_printer = CLOCPrinter.new(@options[:output])
44
+ Reports::CLOCReport.print(plugin_directory, cloc_printer, error_printer)
45
+ end
46
+
47
+ private def output_vulnerability_report(plugin_directory)
48
+ vulnerabilities_printer = VulnerabilitiesPrinter.new(@options[:output])
49
+ Reports::VulnerabilityReport.print(plugin_directory, vulnerabilities_printer, error_printer)
50
+ end
51
+
52
+ private def output_issues_report(plugin_directory)
53
+ printer = IssuesPrinterFactory.create_printer(@options[:issues_format], @options[:hide_ignores], @options[:output])
54
+ Reports::IssuesReport.new(plugin_directory, printer).print
55
+ end
56
+
57
+ private def error_printer
58
+ @error_printer ||= ErrorPrinter.new(@options[:output])
59
+ end
60
+
61
+ private def output_error_list_to_file(plugin_directory)
62
+ error_list_file = @options[:error_list_file]
63
+ fail(IOError, IOError.message(error_list_file.class)) unless error_list_file.respond_to?(:puts)
64
+
65
+ printer = IssuesPrinterFactory.create_printer(:error_list, @options[:hide_ignores], error_list_file)
66
+ Reports::IssuesReport.new(plugin_directory, printer).print
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,9 @@
1
+ module Pluginscan
2
+ class Error < StandardError; end
3
+ class UnknownIssuesFormat < Error; end
4
+ class IOError < Error
5
+ def self.message(file_class)
6
+ "Expected error_list_file to be an I/O object (e.g. a file) which implements `puts`. Got a #{file_class}"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ require 'pluginscan/printer'
2
+
3
+ module Pluginscan
4
+ class ErrorPrinter < Printer
5
+ def print(error)
6
+ print_error_line(error)
7
+ print_blank_line
8
+ end
9
+
10
+ private
11
+
12
+ def print_error_line(error)
13
+ label = "[ERROR]".color(:red)
14
+ @output.puts "#{label} #{error}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ require 'find'
2
+
3
+ module Pluginscan
4
+ # Responsible for searching through a directory for php files, and counting the total files
5
+ class FileFinder
6
+ def initialize(directory)
7
+ @directory = directory
8
+ end
9
+
10
+ def count
11
+ found_files.count
12
+ end
13
+
14
+ def php_files
15
+ found_files.php_files
16
+ end
17
+
18
+ private
19
+
20
+ def found_files
21
+ @found_files ||= find_files
22
+ end
23
+
24
+ def find_files
25
+ found_files = FoundFiles.new
26
+
27
+ Find.find @directory do |file|
28
+ found_files.count += 1 unless Dir.exist?(file) # Skip directories
29
+ found_files.php_files << file if file =~ /\.php$/
30
+ end
31
+
32
+ found_files
33
+ end
34
+
35
+ FoundFiles = Struct.new(:php_files, :count) do
36
+ def initialize
37
+ self.php_files = []
38
+ self.count = 0
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,14 @@
1
+ require 'rainbow'
2
+ require 'rainbow/ext/string'
3
+
4
+ module Pluginscan
5
+ class Printer
6
+ def initialize(output = $stdout)
7
+ @output = output
8
+ end
9
+
10
+ def print_blank_line
11
+ @output.puts ""
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ require 'pluginscan/reports/cloc_report/cloc'
2
+ require 'pluginscan/reports/cloc_report/system_cloc'
3
+ require 'pluginscan/reports/cloc_report/cloc_scanner'
4
+ require 'pluginscan/reports/cloc_report/cloc_printer'
5
+ require 'pluginscan/error_printer'
6
+
7
+ module Pluginscan
8
+ module Reports
9
+ module CLOCReport
10
+ def self.print(plugin_directory, printer, error_printer)
11
+ cloc = CLOCScanner.new.scan(plugin_directory)
12
+ printer.print(cloc)
13
+
14
+ rescue CLOCScanner::Unavailable
15
+ error_printer.print("The 'cloc' command is unavailable. Is it installed and in your path?")
16
+ rescue CLOCScanner::Exception => e
17
+ error_printer.print("An error occurred while calculating the sloccount with CLOC:\n #{e.message}")
18
+ rescue StandardError => e
19
+ # We don't want an error with CLOC to interrupt the rest of pluginscan
20
+ # TODO: not sure this is sensible
21
+ error_printer.print("An error occurred while calculating the sloccount with CLOC:\n #{e.message}")
22
+ else
23
+ return true
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ class CLOC
2
+ class CSVError < StandardError; end
3
+
4
+ def initialize(cloc_csv)
5
+ fail "Not a CSV: #{cloc_csv}" unless cloc_csv.is_a? CSV::Table
6
+ @cloc_csv = cloc_csv
7
+ end
8
+
9
+ def language_counts
10
+ @cloc_csv.map { |row|
11
+ next if row.empty?
12
+ language = row["language"]
13
+ sloc = Integer(row["code"])
14
+ file_count = Integer(row["files"])
15
+ LanguageCount.new(language, sloc, file_count)
16
+ }.compact
17
+ end
18
+
19
+ LanguageCount = Struct.new(:language, :sloc, :file_count)
20
+ end
21
+
@@ -0,0 +1,42 @@
1
+ require 'pluginscan/printer'
2
+
3
+ module Pluginscan
4
+ class CLOCPrinter < Printer
5
+ def print(cloc)
6
+ print_headline
7
+ print_results(cloc)
8
+ print_blank_line
9
+ end
10
+
11
+ private
12
+
13
+ def print_headline
14
+ @output.puts "SLOC counts from the 'CLOC' tool:".color(:blue)
15
+ end
16
+
17
+ def print_results(cloc)
18
+ print_no_result if cloc.language_counts.empty?
19
+
20
+ cloc.language_counts.each{ |language_count| print_result language_count }
21
+ end
22
+
23
+ def print_result(language_count)
24
+ data = {
25
+ language: language_count.language.color(:green),
26
+ sloc: language_count.sloc.to_s.color(:red),
27
+ file_count: language_count.file_count,
28
+ }
29
+ # Field widths need to account for the colouring characters: 9 of them in total
30
+ # e.g. for red this is \e[31m at the beginning and \e[0m at the end,
31
+ # with \e counting as one character:
32
+ # pad sloc to 5 (+9 = 14)
33
+ # pad language to 12 (+9 = 21)
34
+ @output.puts format(" %<language>-21s %<sloc>14s lines across %<file_count>2d files\n", data)
35
+ end
36
+
37
+ def print_no_result
38
+ sadface = ":(".color(:red)
39
+ @output.puts " CLOC didn't find any code #{sadface}"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ require 'csv'
2
+
3
+ # Responsible for running the `cloc` system command and handling any resulting errors
4
+ class CLOCScanner
5
+ class Exception < StandardError; end
6
+ class ArgumentError < RuntimeError; end
7
+ class Unavailable < RuntimeError; end
8
+ class NoDirectory < RuntimeError; end
9
+ class CSVError < RuntimeError; end
10
+
11
+ def initialize(system_cloc = SystemCloc.new)
12
+ @system_cloc = system_cloc
13
+ end
14
+
15
+ def scan(directory)
16
+ fail ArgumentError, "directory must must be a string (or quack like a string)" unless directory.respond_to?(:to_str)
17
+ fail Unavailable, "The 'cloc' command is unavailable. Is it installed and in your path?" unless cloc_available?
18
+ fail NoDirectory, "No such directory: '#{directory}'" unless Dir.exist?(directory)
19
+
20
+ result, status = @system_cloc.call(directory)
21
+
22
+ if status.success?
23
+ CLOC.new(cloc_csv(result))
24
+ else
25
+ raise Exception, "CLOC raised an error we didn't recognise. Here's the output:\n#{result}"
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def cloc_csv(cloc_result)
32
+ CSV.parse(cloc_result.lstrip, headers: true)
33
+ rescue CSV::MalformedCSVError => e
34
+ raise CSVError, "The CSV generated by CLOC was malformed: #{e}"
35
+ end
36
+
37
+ def cloc_available?
38
+ _result, status = @system_cloc.which
39
+ status.success?
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # A thin wrapper around the `cloc` system call
2
+ # The CLOC project is at http://cloc.sourceforge.net/ and can be installed on OSX using homebrew:
3
+ # brew install cloc
4
+ #
5
+ # DANGER: not covered by tests
6
+ class SystemCloc
7
+ def initialize(command_name = 'cloc')
8
+ @command_name = command_name
9
+ end
10
+
11
+ def which
12
+ # DANGER!!! calling system command
13
+ Open3.capture2("which #{@command_name}")
14
+ end
15
+
16
+ def call(directory)
17
+ # DANGER!!! calling system command
18
+ # Should be safe from injection because of the `Dir.exist?` check.
19
+ Open3.capture2("#{@command_name} --csv --quiet #{directory} 2> #{error_log_name}")
20
+ ensure
21
+ delete_empty_error_log
22
+ end
23
+
24
+ private
25
+
26
+ def error_log_name
27
+ "#{@command_name}_error.log"
28
+ end
29
+
30
+ def delete_empty_error_log
31
+ File.delete(error_log_name) if File.zero?(error_log_name)
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ require 'pluginscan/file_finder'
2
+ require 'pluginscan/reports/issues_report/issues_scanner'
3
+ require 'pluginscan/reports/issues_report/issue_checks'
4
+
5
+ module Pluginscan
6
+ module Reports
7
+ class IssuesReport
8
+ def initialize(plugin_directory, printer = IssuesPrinter.new)
9
+ found_files = FileFinder.new(plugin_directory)
10
+ issues = IssuesScanner.new(THE_CHECKS).scan(found_files.php_files)
11
+ @data = {
12
+ issues: issues,
13
+ file_count: found_files.count,
14
+ }
15
+ @printer = printer
16
+ end
17
+
18
+ def print
19
+ @printer.print(@data)
20
+ true
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,99 @@
1
+ module Pluginscan
2
+ class ErrorListPrinter < Printer
3
+ def initialize(hide_ignores = false, output = $stdout)
4
+ @hide_ignores = hide_ignores
5
+ @output = output
6
+ @line_printer = ErrorLinePrinter.new
7
+ end
8
+
9
+ def print(data)
10
+ issues = data[:issues]
11
+ @output.puts error_lines(issues)
12
+ end
13
+
14
+ # TODO: this should be the print method; return an array of lines, and let the caller be responsible for outputting it
15
+ def error_lines(issues)
16
+ issues.inject([]) do |output, (file, file_findings)|
17
+ output + file_error_lines(file, file_findings)
18
+ end
19
+ end
20
+
21
+ private def file_error_lines(file, file_findings)
22
+ file_findings.inject([]) do |checks_output, check_findings|
23
+ checks_output + check_output(file, check_findings.check, check_findings.findings)
24
+ end
25
+ end
26
+
27
+ private def check_output(file, check, findings)
28
+ findings.reject!(&:ignored) if @hide_ignores
29
+ findings.map do |finding|
30
+ error_line = ErrorLineFactory.build(file, check.name, finding)
31
+ @line_printer.print(error_line)
32
+ end
33
+ end
34
+ end
35
+
36
+ class ErrorLinePrinter
37
+ def print(el)
38
+ "\"#{el.file}\", line #{el.line_number}, col #{el.column_number}: #{el.message}"
39
+ end
40
+ end
41
+
42
+ # This is almost like a view model: exposes methods for use in a template
43
+ class ErrorLine
44
+ attr_reader :file
45
+
46
+ def initialize(file, check_name, finding)
47
+ @file = file
48
+ @check_name = check_name
49
+ @finding = PrintableFinding.new(finding)
50
+ end
51
+
52
+ def line_number
53
+ # TODO: why is the original called lineno?? That's a rubbish name!
54
+ @finding.lineno
55
+ end
56
+
57
+ def column_number
58
+ @finding.col_number
59
+ end
60
+
61
+ def message
62
+ "[#{@check_name}] #{@finding.line}"
63
+ end
64
+ end
65
+
66
+ class IgnoredErrorLine < ErrorLine
67
+ # TODO: would decoration be better here?
68
+ def message
69
+ super.sub("]", "][IGNORE]")
70
+ end
71
+ end
72
+
73
+ class ErrorLineFactory
74
+ def self.build(file, check_name, finding)
75
+ klass(finding.ignored).new(file, check_name, finding)
76
+ end
77
+
78
+ def self.klass(ignored)
79
+ ignored ? IgnoredErrorLine : ErrorLine
80
+ end
81
+ end
82
+
83
+ class PrintableFinding < SimpleDelegator
84
+ def col_number
85
+ # Seems like vim treats tabs as single spaces for the purposes of calculating columns - at least on my setup
86
+ # so we don't need to expand the tabs
87
+ __getobj__.line.index(match) + 1
88
+ end
89
+
90
+ def line
91
+ escape_special_chars(__getobj__.line.strip)
92
+ end
93
+
94
+ private def escape_special_chars(string)
95
+ # Vim interprets `:` as a delimiter
96
+ string.gsub(":", '\:')
97
+ end
98
+ end
99
+ end