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,382 @@
1
+ require "pluginscan/reports/issues_report/issue_checks/check"
2
+ require "pluginscan/reports/issues_report/issue_checks/function_check"
3
+ require "pluginscan/reports/issues_report/issue_checks/variable_check"
4
+ require "pluginscan/reports/issues_report/issue_checks/variable_safety_checker"
5
+ require "pluginscan/reports/issues_report/issue_checks/comment_checker"
6
+
7
+ module Pluginscan
8
+ THE_CHECKS = [
9
+ #
10
+ # Look for superglobal arrays. Use of data from these arrays should be checked for unsanitised or unescaped use.
11
+ #
12
+ VariableCheck.new(
13
+ name: 'Superglobal',
14
+ message: 'Superglobal requires manual review',
15
+ variables: %w(
16
+ $_GET
17
+ $_POST
18
+ $_SERVER
19
+ $_REQUEST
20
+ $_COOKIE
21
+ $_ENV
22
+ $_FILES
23
+ ),
24
+ ),
25
+
26
+ #
27
+ # Check for SQLi
28
+ #
29
+ Check.new(
30
+ name: 'Database access',
31
+ message: 'Database access requires manual review',
32
+ patterns: [
33
+ /\$wpdb/,
34
+ ],
35
+ ignores: [
36
+ /global.+\$wpdb/,
37
+ lambda do |line|
38
+ # Try to exclude some things that are definitely not injectable
39
+ # safe_things = %w(prefix postmeta comments commentmeta links options posts terms term_relationships term_taxonomy usermeta users blogs blog_versions registration_log signups site sitecategories sitemeta).each do |safe|
40
+ # line.gsub!(/\$wpdb->#{safe}/, '')
41
+ # end
42
+
43
+ # If it doesn't look like the whole call is on this line, all bets are off
44
+ return false if line.count('(') != line.count(')')
45
+
46
+ # If it looks like they might be concatenating a string, all bets are off
47
+ return false if line.match(/["']{1}\s*\./) || line.match(/\.\s['"]{1}/)
48
+
49
+ # If the first occurrence of $wpdb is a function call ($wpdb->thing())
50
+ # then we don't care about it or anything before it
51
+ line = line.gsub(/^.*?\$wpdb-/, '')
52
+
53
+ # If there's anything before the first wpdb, which can't result in it getting
54
+ # assigned to something else, then we can delete all of that
55
+ line = line.gsub(/^[^(=]*?\$wpdb/, '')
56
+
57
+ # $wpdb->whatever is safe
58
+ line = line.gsub(Regexp.new(Regexp.escape("$wpdb->")), '')
59
+
60
+ # Are there any variables left?
61
+ !line.match(/\$[a-z]{1}[a-z0-9_]*/)
62
+ end,
63
+ ]
64
+ ),
65
+
66
+ FunctionCheck.new(
67
+ name: 'MySQL functions',
68
+ message: 'People shouldn\'t use MySQL functions in WordPress.',
69
+ patterns: [
70
+ /mysqli?_[a-z0-9_]+/,
71
+ ]
72
+ ),
73
+
74
+ #
75
+ # Arbitrary code execution
76
+ #
77
+ FunctionCheck.new(
78
+ name: 'PHP code generation',
79
+ message: 'Code generation functions might be bad',
80
+ function_names: %w(
81
+ eval
82
+ create_function
83
+ assert
84
+ ),
85
+ ),
86
+
87
+ FunctionCheck.new(
88
+ name: 'User-controllable function calls',
89
+ message: 'What functions are they calling?',
90
+ patterns: [
91
+ /\$\$[a-zA-Z0-9_]+\s*\(/,
92
+ ],
93
+ function_names: %w(
94
+ call_user_func
95
+ call_user_func_array
96
+ ),
97
+ ),
98
+
99
+ #
100
+ # Execution of system commands
101
+ #
102
+ FunctionCheck.new(
103
+ name: 'System calls',
104
+ message: 'Executing system commands might be bad',
105
+ patterns: [
106
+ /`.+`/,
107
+ ],
108
+ function_names: %w(
109
+ popen
110
+ expect_popen
111
+ proc_optioni
112
+ exec
113
+ shell_exec
114
+ system
115
+ passthru
116
+ ),
117
+ ),
118
+
119
+ #
120
+ # File manipulation: arbitrary code, altering system configs, info leakage
121
+ #
122
+ FunctionCheck.new(
123
+ name: 'File operations',
124
+ message: 'File operations require manual review',
125
+ patterns: [
126
+ /xdiff_[a-z0-9_]+/,
127
+ ],
128
+ function_names: %w(
129
+ bzwrite
130
+ chmod
131
+ chgrp
132
+ chown
133
+ copy
134
+ file_put_contents
135
+ fputscv
136
+ fputs
137
+ fprintf
138
+ ftruncate
139
+ fwrite
140
+ gzwrite
141
+ gzputs
142
+ loadXML
143
+ makedir
144
+ move_uploaded_file
145
+ rename
146
+ unlink
147
+ vfprintf
148
+ yaml_emit_file
149
+ fread
150
+ fget
151
+ fgets
152
+ fgetss
153
+ fgetc
154
+ fpassthru
155
+ glob
156
+ file_get_contents
157
+ fgetcsv
158
+ file
159
+ bzread
160
+ gzread
161
+ gzgets
162
+ gzgetss
163
+ gzgetc
164
+ gzpassthru
165
+ finfo_file
166
+ highlight_file
167
+ show_source
168
+ readlink
169
+ ),
170
+ ),
171
+
172
+ #
173
+ # Attempts to obfuscate badware
174
+ #
175
+ FunctionCheck.new(
176
+ name: 'Possible obfuscation',
177
+ message: 'Are these obfuscating code or other badstuff?',
178
+ function_names: %w(
179
+ str_rot13
180
+ uudecode
181
+ base64_decode
182
+ base64_encode
183
+ ),
184
+ ),
185
+
186
+ #
187
+ # Network activity - remote shells, information leakage
188
+ #
189
+ FunctionCheck.new(
190
+ name: 'Network functions',
191
+ message: 'Who\'s sending what and where?',
192
+ patterns: [
193
+ /ftp_[a-z0-9_]+/,
194
+ /socket_[a-z0-9_]+/,
195
+ ],
196
+ function_names: %w(
197
+ curl_exec
198
+ curl_setopt
199
+ fsockopen
200
+ get_headers
201
+ wp_get_http
202
+ wp_get_http_headers
203
+ wp_remote_get
204
+ wp_remote_post
205
+ wp_remote_request
206
+ wp_remote_head
207
+ ),
208
+ ),
209
+
210
+ #
211
+ # Potentially unsafe wordpress functions
212
+ #
213
+ FunctionCheck.new(
214
+ name: 'Potentially unsafe wordpress functions',
215
+ message: 'Some wordpress functions are not inherently safe, this functions return value may not be safe.',
216
+ function_names: %w(
217
+ wp_unslash
218
+ wp_get_referer
219
+ wp_get_original_referer
220
+ esc_sql
221
+ ),
222
+ ),
223
+
224
+ #
225
+ # PHP object injection
226
+ #
227
+ FunctionCheck.new(
228
+ name: 'PHP object injection',
229
+ message: "If an object can be created where this function does something dangerous, that's bad",
230
+ function_names: %w(
231
+ __wakeup
232
+ __destruct
233
+ __toString
234
+ unserialize
235
+ ),
236
+ ),
237
+
238
+ #
239
+ # These functions parse strings into variables
240
+ #
241
+ FunctionCheck.new(
242
+ name: 'Variable parsing',
243
+ message: 'These functions parse strings into varables. Could be used as part of an Arbitrary Code Execution',
244
+ function_names: %w(
245
+ parse_str
246
+ extract
247
+ ),
248
+ ),
249
+
250
+ #
251
+ # Using primitives where the WordPress API provides a better alternative
252
+ #
253
+ FunctionCheck.new(
254
+ name: 'Redundant functions',
255
+ message: 'People using these should probably be using the WordPress API instead',
256
+ function_names: %w(
257
+ htmlspecialchars
258
+ mail
259
+ ),
260
+ # Should we ignore user-created mail() functions?
261
+ # Or is the point of this test to check for those? -dgms
262
+ ),
263
+
264
+ #
265
+ # What it says on the tin. WTF. lolwat.
266
+ #
267
+ FunctionCheck.new(
268
+ name: 'Batshit weird',
269
+ message: 'What on earth is it doing?',
270
+ patterns: [
271
+ /runkit_[a-z0-9_]+/,
272
+ /ldap_[a-z0-9_]+/,
273
+ ],
274
+ function_names: %w(
275
+ ini_set
276
+ put_env
277
+ sleep
278
+
279
+ apache_set_env
280
+
281
+ session_decode
282
+ ),
283
+ ),
284
+
285
+ #
286
+ # Accidents
287
+ #
288
+ Check.new(
289
+ name: 'Accidents',
290
+ message: 'Did someone do a silly?',
291
+ patterns: [
292
+ /\btodo\b/i,
293
+ /\bfixme\b/i,
294
+ /\bfuck\b/i,
295
+ /\bshit\b/i,
296
+ /\bcrap\b/i,
297
+ /\bbroken\b/i,
298
+ /\bhack\b/i,
299
+ /\bxxx\b/i,
300
+ /\bugly\b/i,
301
+ /\bscary\b/i,
302
+ /\bwtf\b/i,
303
+ /\bsecurity\b/i,
304
+ /\bbodge\b/i,
305
+ /\bhardening\b/i,
306
+ ]
307
+ ),
308
+
309
+ #
310
+ # Security Policy
311
+ #
312
+ Check.new(
313
+ name: 'Inline JavaScript',
314
+ message: 'Is there inline javascript which would prevent us enforcing a content security policy?',
315
+ patterns: [
316
+ /<script/i,
317
+ ],
318
+ ignores: [
319
+ /<script[^>]*\ +src=/i # tags containing src don't include inline JS
320
+ ]
321
+ ),
322
+
323
+ Check.new(
324
+ name: 'Inline CSS',
325
+ message: 'Is there inline css which would prevent us enforcing a content security policy?',
326
+ patterns: [
327
+ /<style/i,
328
+ ],
329
+ ),
330
+
331
+ Check.new(
332
+ name: 'Event Attributes',
333
+ message: 'Are there HTML event attributes which can execute inline JavaScript and thus prevent us enforcing a content security policy?',
334
+ patterns: [
335
+ /<[^>]* (on[[:alpha:]]+)=/i, # All event attributes start with "on" e.g. "onclick"
336
+ ],
337
+ ),
338
+
339
+ #
340
+ # Direct Loading of files
341
+ #
342
+ Check.new(
343
+ name: 'Direct loading',
344
+ message: 'Is someone trying to make a file that can be included directly?',
345
+ patterns: [
346
+ /wp-load\.php/,
347
+ ],
348
+ ),
349
+
350
+ #
351
+ # Unreliable IP addresses
352
+ #
353
+ Check.new(
354
+ name: 'Unreliable IP',
355
+ message: 'Are they making unsafe assumptions about client IP addresses?',
356
+ patterns: [
357
+ /HTTP_X_FORWARDED_FOR/,
358
+ /HTTP_CLIENT_IP/,
359
+ /HTTP_X_CLUSTER_CLIENT_IP/,
360
+ /HTTP_X_FORWARDED/,
361
+ /HTTP_FORWARDED_FOR/,
362
+ /HTTP_CF_CONNECTING_IP/,
363
+ /HTTP_X_REAL_IP/,
364
+ /HTTP_FORWARDED/,
365
+ ]
366
+ ),
367
+
368
+ #
369
+ # Version leaks
370
+ #
371
+ FunctionCheck.new(
372
+ name: 'Version leaks',
373
+ message: 'Are they leaking secrets to the enemy?',
374
+ function_names: [
375
+ 'phpversion',
376
+ ],
377
+ patterns: [
378
+ /(?:get_)?bloginfo\(.*?version/,
379
+ ],
380
+ ),
381
+ ].freeze
382
+ end
@@ -0,0 +1,55 @@
1
+ module Pluginscan
2
+ # Responsible for checking a string against set of rexgexps and returning all matches
3
+ class Check
4
+ attr_reader :name, :message, :patterns, :ignores
5
+
6
+ def initialize(check_hash)
7
+ @name = check_hash[:name]
8
+ @message = check_hash[:message]
9
+ @patterns = Array(check_hash[:patterns])
10
+ @ignores = Array(check_hash[:ignores]).map do |ignore_thing|
11
+ IgnoreThing.new(ignore_thing)
12
+ end
13
+ end
14
+
15
+ def run(content)
16
+ pattern_matches(content).map{ |matchdata| match(matchdata) }
17
+ end
18
+
19
+ def ignore?(_match, content)
20
+ # `match` is not used in the default case, but is included
21
+ # so that classes inheriting from this one can use it when deciding
22
+ # whether or not to ignore content by overriding this function
23
+ ignores.any? { |ignore_thing| ignore_thing.ignore?(content) }
24
+ end
25
+
26
+ private
27
+
28
+ def pattern_matches(content)
29
+ @patterns.map { |pattern| content.match(pattern) }.compact
30
+ end
31
+
32
+ IgnoreThing = Struct.new(:ignore_thing) do
33
+ def ignore?(content)
34
+ case ignore_thing
35
+ when Regexp
36
+ content.match(ignore_thing)
37
+ when Proc
38
+ ignore_thing.call(content)
39
+ end
40
+ end
41
+ end
42
+
43
+ def match(matchdata)
44
+ # Select the string we were interested in from the matchdata
45
+ matchdata.to_a.last
46
+ # This will return what we want assuming that we have either:
47
+ # a single item from a simple regex:
48
+ # #<MatchData "$_POST">.to_a
49
+ # => ["$_POST"]
50
+ # a whole match and a specific match, from a function_list item:
51
+ # #<MatchData "@unlink(" 1:"unlink">.to_a
52
+ # => ["@unlink(", "unlink"]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ module Pluginscan
2
+ # Responsible for deciding whether usages of a variable in a string are in a comment
3
+ module CommentChecker
4
+ COMMENT_REGEXPS = [
5
+ %r{^\s*//}, # line comment
6
+ /^\s*\*/, # block comment
7
+ ].freeze
8
+
9
+ def self.commented?(content)
10
+ COMMENT_REGEXPS.any? { |regexp| content.match(regexp) }
11
+ end
12
+ end
13
+ end