pluginscan 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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
+