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,32 @@
1
+ module Pluginscan
2
+ # Extends Check with helpers for making patterns and ignores based on a list of function names
3
+ class FunctionCheck < Check
4
+ def initialize(check_hash)
5
+ check_hash.default = []
6
+ check_hash[:patterns] = self.class.patterns(check_hash[:patterns], check_hash[:function_names])
7
+ check_hash[:ignores] = self.class.ignores(check_hash[:ignores], check_hash[:function_names])
8
+ super(check_hash)
9
+ end
10
+
11
+ def self.function_list(functions)
12
+ # ^ Start of line
13
+ # [^a-z0-9|_] Characters which would imply that the word isn't the whole function name
14
+ # \s* any amount of whitespace
15
+ # \\( a literal open bracket - i.e. the start of the function arguments
16
+ functions.map{ |function| Regexp.new("(^|[^a-z0-9|_])(#{function})\s*\\(") }
17
+ end
18
+
19
+ def self.function_ignores(functions)
20
+ # If the author is creating a similarly named function then that should be ignored (?)
21
+ functions.map { |function| Regexp.new("function\s+[^a-z0-9|_]?#{function}\s*\\(") }
22
+ end
23
+
24
+ private def self.patterns(patterns, function_names)
25
+ Array(patterns) + function_list(function_names)
26
+ end
27
+
28
+ private def self.ignores(ignores, function_names)
29
+ Array(ignores) + function_ignores(function_names) + CommentChecker::COMMENT_REGEXPS
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ module Pluginscan
2
+ # Extends Check with helpers for making patterns and ignores based on a list of variables or constantst
3
+ class VariableCheck < Check
4
+ def initialize(check_hash)
5
+ super(check_hash)
6
+ @patterns = Array(check_hash[:variables]).map{ |var| Regexp.new(Regexp.escape(var)) }
7
+ end
8
+
9
+ def ignore?(variable, content)
10
+ VariableSafetyChecker.new.all_safe?(variable, content) || CommentChecker.commented?(content)
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,112 @@
1
+ module Pluginscan
2
+ # Responsible for deciding whether usages of a variable in a string are safe
3
+ class VariableSafetyChecker
4
+ # Functions which, if they surround the variable, make it safe
5
+ # because they return a boolean and not the value of the varaible
6
+ SAFE_FUNCTIONS = [
7
+ "isset",
8
+ "empty",
9
+ "in_array",
10
+ "strpos",
11
+ "strlen",
12
+ "if",
13
+ "switch",
14
+ "is_email",
15
+
16
+ # PHP typechecks - seen in the wild:
17
+ "is_array",
18
+
19
+ # PHP typechecks - not seen in the wild:
20
+ "is_bool",
21
+ "is_callable",
22
+ "is_double",
23
+ "is_float",
24
+ "is_int",
25
+ "is_integer",
26
+ "is_long",
27
+ "is_null",
28
+ "is_numeric",
29
+ "is_object",
30
+ "is_real",
31
+ "is_resource",
32
+ "is_scalar",
33
+ "is_string",
34
+
35
+ "intval",
36
+ "absint",
37
+ "wp_verify_nonce",
38
+ "count",
39
+ "sizeof",
40
+ "unset",
41
+
42
+ # Candidates for inclusion - not seen in the wild:
43
+ # "gettype",
44
+ # "settype",
45
+ # "boolval",
46
+ # "doubleval", # might match eval?
47
+ # "floatval",
48
+ ].freeze
49
+
50
+ # Infixes which, if they are used around the variable, make it safe,
51
+ # because they are checking the value, not returning it
52
+ SAFE_INFIXES = [
53
+ '==',
54
+ '===',
55
+ '!=',
56
+ '!==',
57
+ '<',
58
+ '>',
59
+ '<=',
60
+ '>=',
61
+ ].freeze
62
+
63
+ INFIX_CHARS = %w(= < > !).freeze
64
+
65
+ def all_safe?(variable, content)
66
+ match_count(variable, content) <= safe_count(variable, content)
67
+ end
68
+
69
+ def match_count(variable, content)
70
+ content.scan(variable).count # `scan` returns ALL matches
71
+ end
72
+
73
+ def safe_count(variable, content)
74
+ safe_function_count(variable, content) +
75
+ safe_infix_count(variable, content)
76
+ end
77
+
78
+
79
+
80
+ # The number of matches which are safe by being wrapped in a function
81
+ private def safe_function_count(variable, content)
82
+ SAFE_FUNCTIONS.map { |function|
83
+ wrapped_in_function_count(function, variable, content)
84
+ }.inject(:+)
85
+ end
86
+
87
+ # The number of matches which are safe by being checked in an infix
88
+ private def safe_infix_count(variable, content)
89
+ SAFE_INFIXES.map { |infix|
90
+ used_in_infix_check_count(infix, variable, content)
91
+ }.inject(:+)
92
+ end
93
+
94
+ # TODO: the below methods feel private, but are directly tested
95
+ # That makes me feel like there's an object to be extracted here
96
+ def used_in_infix_check_count(infix, variable, content)
97
+ variable = Regexp.escape variable
98
+ infix = Regexp.escape infix
99
+ non_infix = "[^#{Regexp.escape INFIX_CHARS.join}]"
100
+
101
+ equals_before_regexp = /#{non_infix}#{infix}\ *#{variable}\ *\[/
102
+ equals_after_regexp = /#{variable}\ *\[[^\[]+\]\ *#{infix}#{non_infix}/
103
+ content.scan(equals_before_regexp).count +
104
+ content.scan(equals_after_regexp).count
105
+ end
106
+
107
+ def wrapped_in_function_count(function_name, variable, content)
108
+ variable = Regexp.escape variable
109
+ content.scan(/#{function_name}\ *\(\ *#{variable}[^)]*\)/).count
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,29 @@
1
+ module Pluginscan
2
+ Finding = Struct.new(:lineno, :line, :match, :ignored)
3
+
4
+ class CheckFindings
5
+ attr_reader :findings
6
+ attr_reader :check
7
+
8
+ def initialize(check)
9
+ @check = check
10
+ @findings = []
11
+ end
12
+
13
+ def add(more_findings)
14
+ @findings += more_findings
15
+ end
16
+
17
+ def any_findings?
18
+ !findings.empty?
19
+ end
20
+
21
+ def all_ignored?
22
+ @findings.all?(&:ignored)
23
+ end
24
+
25
+ def count
26
+ findings.count
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module Pluginscan
2
+ # Wraps a couple of helper methods around an array of issues
3
+ class Issues
4
+ include Enumerable
5
+
6
+ def initialize(issues)
7
+ @issues = issues
8
+ end
9
+
10
+ def each(&block)
11
+ @issues.each(&block)
12
+ end
13
+
14
+ def scanned_files_count
15
+ @issues.count
16
+ end
17
+
18
+ def found_problems_count
19
+ found_problems.reduce(0){ |count, (_file, file_findings)| count + file_findings.map(&:count).reduce(:+) }
20
+ end
21
+
22
+ private def files
23
+ @issues.keys
24
+ end
25
+
26
+ private def found_problems
27
+ # Ignore files which didn't have any problems
28
+ Issues.new(@issues.select { |_file, file_findings| !file_findings.empty? })
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ require 'pluginscan/printer'
2
+ require 'pluginscan/reports/issues_report/issues_printer/file_issues_printer'
3
+ require 'pluginscan/reports/issues_report/issues_printer/check_findings_printer'
4
+ require 'pluginscan/reports/issues_report/issues_printer/finding_printer'
5
+
6
+ module Pluginscan
7
+ class IssuesPrinter < Printer
8
+ def initialize(hide_ignores, output = $stdout)
9
+ @hide_ignores = hide_ignores
10
+ @output = output
11
+ end
12
+
13
+ def print(data)
14
+ issues = data[:issues]
15
+ file_count = data[:file_count]
16
+ print_headline(issues, file_count)
17
+ print_results(issues)
18
+ end
19
+
20
+ private
21
+
22
+ def print_headline(issues, file_count)
23
+ @output.puts "Scanned #{issues.scanned_files_count} out of #{file_count} files and found #{issues.found_problems_count} things:".color(:blue)
24
+ end
25
+
26
+ def print_results(issues)
27
+ printer = FileIssuesPrinter.new(@hide_ignores, @output)
28
+ issues.each do |file, findings|
29
+ findings = findings.reject(&:all_ignored?) if @hide_ignores
30
+ printer.print(file, findings)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ module Pluginscan
2
+ # Responsible for printing a check description and all of the
3
+ # findings associated with that check
4
+ class CheckFindingsPrinter < Printer
5
+ def print(check, findings)
6
+ return if findings.empty? && check.name != 'Encoding' # Encoding deliberately has no findings because its a file-level check. TODO: Find a better way to handle this. The benefits of filtering out findings before we try and print them are significant.
7
+
8
+ @output.puts CheckView.new(check).title_line
9
+ print_findings(findings)
10
+ print_blank_line
11
+ end
12
+
13
+ private
14
+
15
+ def print_findings(findings)
16
+ printer = FindingPrinter.new(@output)
17
+ findings.each do |finding|
18
+ printer.print(finding)
19
+ end
20
+ end
21
+ end
22
+
23
+ # Decorate Check with view-specific methods
24
+ class CheckView < SimpleDelegator
25
+ def initialize(check)
26
+ @check = check
27
+ super
28
+ end
29
+
30
+ def title_line
31
+ name = "#{@check.name}:".color(:red)
32
+ message = @check.message.color(:yellow)
33
+
34
+ " #{name} #{message}"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ require 'delegate' # This shouldn't (?) be required - should be autoloaded, but apparently isn't
2
+
3
+ module Pluginscan
4
+ # Print a findings report for an individual file
5
+ class FileIssuesPrinter < Printer
6
+ def initialize(hide_ignores, output = $stdout)
7
+ @hide_ignores = hide_ignores
8
+ @output = output
9
+ end
10
+
11
+ def print(file, checks_findings)
12
+ return if checks_findings.empty?
13
+
14
+ @output.puts FileView.new(file).file_path
15
+ print_findings(checks_findings)
16
+ # Doesn't need a blank line: each block of findings ends with a blank line
17
+ end
18
+
19
+ private
20
+
21
+ def print_findings(checks_findings)
22
+ printer = CheckFindingsPrinter.new(@output)
23
+ checks_findings.each do |check_findings|
24
+ findings = check_findings.findings
25
+ findings.reject!(&:ignored) if @hide_ignores
26
+ printer.print(check_findings.check, findings)
27
+ end
28
+ end
29
+ end
30
+
31
+ class FileView < SimpleDelegator
32
+ def file_path
33
+ color(:green)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ module Pluginscan
2
+ # Responsible for printing a single finding on the command line
3
+ class FindingPrinter < Printer
4
+ def print(finding)
5
+ finding = FindingView.new(finding)
6
+ finding = IgnoredFindingView.new(finding) if finding.ignored
7
+ @output.puts finding.source_line
8
+ end
9
+ end
10
+
11
+ class FindingView < SimpleDelegator
12
+ def source_line
13
+ data = {
14
+ lineno: lineno,
15
+ line: highlight_matches(line.strip),
16
+ }
17
+
18
+ # pad line number to 5. Because some plugin files really are over 10k lines
19
+ format " %<lineno>5s: %{line}\n", data
20
+ end
21
+
22
+ private
23
+
24
+ def highlight_matches(line)
25
+ return line unless match # TODO: nil checks are evil!
26
+
27
+ line.gsub match, match.color(:cyan)
28
+ end
29
+ end
30
+
31
+ class IgnoredFindingView < SimpleDelegator
32
+ def source_line
33
+ i_symbol = "[I]".color(:red)
34
+
35
+ super.gsub(/^ /, " #{i_symbol}")
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ require 'pluginscan/reports/issues_report/issues_printer'
2
+ require 'pluginscan/reports/issues_report/error_list_printer'
3
+
4
+ module Pluginscan
5
+ # Responsible for creating an object which can print out the list of issues
6
+ # in one of several different ways
7
+ class IssuesPrinterFactory
8
+ def self.create_printer(issues_format, hide_ignores = false, output = $stdout)
9
+ case issues_format
10
+ when :report
11
+ IssuesPrinter.new(hide_ignores, output)
12
+ when :error_list
13
+ ErrorListPrinter.new(hide_ignores, output)
14
+ else
15
+ fail Pluginscan::UnknownIssuesFormat, "Unknown issues formatter '#{issues_format}'"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ require 'pluginscan/reports/issues_report/issues_scanner/file_issues_scanner'
2
+ require 'pluginscan/reports/issues_report/issues_scanner/line_issues_scanner'
3
+ require 'pluginscan/reports/issues_report/issues_scanner/utf8_checker'
4
+ require 'pluginscan/reports/issues_report/issues_models/check_findings'
5
+ require 'pluginscan/reports/issues_report/issues_models/issues'
6
+
7
+ module Pluginscan
8
+ class IssuesScanner
9
+ def initialize(checks)
10
+ file_issues_scanner = FileIssuesScanner.new(checks)
11
+ @file_scanner = FileScanner.new(file_issues_scanner)
12
+ end
13
+
14
+ def scan(files)
15
+ issues = scan_files(files)
16
+
17
+ Issues.new(issues)
18
+ end
19
+
20
+ private
21
+
22
+ def scan_files(file_paths)
23
+ file_paths.inject({}) do |issues, file_path|
24
+ issues.merge file_path => scan_file(file_path)
25
+ end
26
+ end
27
+
28
+ def scan_file(file_path)
29
+ file_contents = File.read(file_path)
30
+ @file_scanner.scan(file_contents)
31
+ end
32
+ end
33
+
34
+ # Responsible for checking that a file's contents is valid
35
+ # and if so, running an issues scan on it
36
+ class FileScanner
37
+ def initialize(issues_scanner)
38
+ @issues_scanner = issues_scanner
39
+ end
40
+
41
+ def scan(file_contents)
42
+ # Check file contents are valid UTF-8 to avoid exceptions later
43
+ invalid_utf8 = UTF8Checker.new.check(file_contents)
44
+ return Array(invalid_utf8) if invalid_utf8
45
+
46
+ @issues_scanner.scan(file_contents)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ require 'stringio'
2
+
3
+ module Pluginscan
4
+ # Responsible for scanning a file for a set of issue types
5
+ class FileIssuesScanner
6
+ attr_reader :file_results
7
+
8
+ def initialize(checks)
9
+ @checks = checks
10
+ end
11
+
12
+ # Returns an array of CheckFindings objects
13
+ def scan(file_contents)
14
+ # Run each check on the file
15
+ checks_findings = @checks.map { |check| LinesIssuesScanner.new(check).scan(file_contents) }
16
+ checks_findings.select(&:any_findings?)
17
+ end
18
+ end
19
+ end
20
+
21
+ module Pluginscan
22
+ # Responsible for scanning each line of a file for issues of a certain type
23
+ class LinesIssuesScanner
24
+ def initialize(check)
25
+ @line_issues_scanner = LineIssuesScanner.new(check)
26
+ @check_findings = CheckFindings.new(check)
27
+ end
28
+
29
+ def scan(file_contents)
30
+ string_io = StringIO.new(file_contents)
31
+ @check_findings.tap do |check_findings|
32
+ string_io.each_line do |line|
33
+ check_findings.add @line_issues_scanner.scan(line, string_io.lineno)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+