wpscan 3.8.28 → 4.0.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 (252) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +104 -30
  3. data/app/app.rb +26 -0
  4. data/app/controllers/aliases.rb +2 -2
  5. data/app/controllers/authenticated_inventory.rb +43 -0
  6. data/app/controllers/core/cli_options.rb +151 -0
  7. data/app/controllers/core.rb +200 -25
  8. data/app/controllers/custom_directories.rb +1 -1
  9. data/app/controllers/enumeration/cli_options.rb +21 -31
  10. data/app/controllers/enumeration/enum_methods.rb +145 -38
  11. data/app/controllers/enumeration.rb +26 -3
  12. data/app/controllers/interesting_findings.rb +25 -0
  13. data/app/controllers/main_theme.rb +1 -1
  14. data/app/controllers/password_attack.rb +14 -6
  15. data/app/controllers/vuln_api.rb +9 -3
  16. data/app/controllers/wp_version.rb +1 -1
  17. data/app/controllers.rb +1 -0
  18. data/app/finders/backup_folders/known_locations.rb +66 -0
  19. data/app/finders/backup_folders.rb +19 -0
  20. data/app/finders/config_backups/known_filenames.rb +6 -4
  21. data/app/finders/config_backups.rb +1 -1
  22. data/app/finders/db_exports/known_locations.rb +16 -14
  23. data/app/finders/db_exports.rb +1 -1
  24. data/app/finders/interesting_findings/backup_db.rb +1 -1
  25. data/app/finders/interesting_findings/debug_log.rb +1 -1
  26. data/app/finders/interesting_findings/duplicator_installer_log.rb +1 -1
  27. data/app/finders/interesting_findings/emergency_pwd_reset_script.rb +1 -1
  28. data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
  29. data/app/finders/interesting_findings/full_path_disclosure.rb +1 -1
  30. data/app/finders/interesting_findings/headers.rb +17 -0
  31. data/app/finders/interesting_findings/mu_plugins.rb +1 -1
  32. data/app/finders/interesting_findings/multisite.rb +1 -1
  33. data/app/finders/interesting_findings/php_disabled.rb +2 -2
  34. data/app/finders/interesting_findings/readme.rb +1 -1
  35. data/app/finders/interesting_findings/registration.rb +1 -1
  36. data/app/finders/interesting_findings/robots_txt.rb +20 -0
  37. data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
  38. data/app/finders/interesting_findings/tmm_db_migrate.rb +1 -1
  39. data/app/finders/interesting_findings/upload_directory_listing.rb +1 -1
  40. data/app/finders/interesting_findings/upload_sql_dump.rb +2 -2
  41. data/app/finders/interesting_findings/wp_cron.rb +1 -1
  42. data/app/finders/interesting_findings/xml_rpc.rb +61 -0
  43. data/app/finders/interesting_findings.rb +13 -4
  44. data/app/finders/main_theme/css_style_in_homepage.rb +1 -1
  45. data/app/finders/main_theme/urls_in_homepage.rb +3 -7
  46. data/app/finders/main_theme/woo_framework_meta_generator.rb +4 -4
  47. data/app/finders/main_theme.rb +1 -1
  48. data/app/finders/medias/attachment_brute_forcing.rb +2 -2
  49. data/app/finders/medias.rb +1 -1
  50. data/app/finders/passwords/wp_login.rb +2 -2
  51. data/app/finders/passwords/xml_rpc.rb +2 -2
  52. data/app/finders/passwords/xml_rpc_multicall.rb +1 -1
  53. data/app/finders/plugin_version/readme.rb +1 -1
  54. data/app/finders/plugin_version.rb +1 -1
  55. data/app/finders/plugins/known_locations.rb +17 -7
  56. data/app/finders/plugins/urls_in_homepage.rb +3 -7
  57. data/app/finders/plugins/wp_json_api.rb +85 -0
  58. data/app/finders/plugins.rb +2 -1
  59. data/app/finders/theme_version/style.rb +1 -1
  60. data/app/finders/theme_version/woo_framework_meta_generator.rb +1 -1
  61. data/app/finders/theme_version.rb +1 -1
  62. data/app/finders/themes/known_locations.rb +12 -6
  63. data/app/finders/themes/urls_in_homepage.rb +3 -7
  64. data/app/finders/themes/wp_json_api.rb +74 -0
  65. data/app/finders/themes.rb +2 -1
  66. data/app/finders/timthumb_version/bad_request.rb +1 -1
  67. data/app/finders/timthumb_version.rb +1 -1
  68. data/app/finders/timthumbs/known_locations.rb +6 -4
  69. data/app/finders/timthumbs.rb +1 -1
  70. data/app/finders/users/author_id_brute_forcing.rb +11 -7
  71. data/app/finders/users/author_posts.rb +1 -1
  72. data/app/finders/users/author_sitemap.rb +1 -1
  73. data/app/finders/users/login_error_messages.rb +1 -1
  74. data/app/finders/users/oembed_api.rb +3 -1
  75. data/app/finders/users/wp_json_api.rb +11 -7
  76. data/app/finders/users.rb +1 -1
  77. data/app/finders/wp_version/atom_generator.rb +1 -1
  78. data/app/finders/wp_version/rdf_generator.rb +1 -1
  79. data/app/finders/wp_version/readme.rb +1 -1
  80. data/app/finders/wp_version/rss_generator.rb +1 -1
  81. data/app/finders/wp_version/unique_fingerprinting.rb +2 -2
  82. data/app/finders/wp_version.rb +1 -1
  83. data/app/finders.rb +1 -0
  84. data/app/formatters/cli.rb +79 -0
  85. data/app/formatters/cli_no_color.rb +9 -0
  86. data/app/formatters/cli_no_colour.rb +17 -0
  87. data/app/formatters/json.rb +14 -0
  88. data/app/formatters/jsonl.rb +29 -0
  89. data/app/formatters/sarif.rb +311 -0
  90. data/app/models/backup_folder.rb +39 -0
  91. data/app/models/fantastico_fileslist.rb +34 -0
  92. data/app/models/headers.rb +44 -0
  93. data/app/models/interesting_finding.rb +41 -2
  94. data/app/models/plugin.rb +8 -2
  95. data/app/models/robots_txt.rb +31 -0
  96. data/app/models/search_replace_db_2.rb +17 -0
  97. data/app/models/theme.rb +9 -2
  98. data/app/models/timthumb.rb +2 -2
  99. data/app/models/user.rb +35 -0
  100. data/app/models/version.rb +49 -0
  101. data/app/models/wp_item/wordpress_org_data.rb +55 -0
  102. data/app/models/wp_item.rb +109 -9
  103. data/app/models/wp_version.rb +2 -2
  104. data/app/models/xml_rpc.rb +73 -3
  105. data/app/models.rb +2 -1
  106. data/app/user_agents.txt +46 -0
  107. data/app/views/cli/core/banner.erb +3 -3
  108. data/app/views/cli/core/finished.erb +15 -0
  109. data/app/views/cli/core/help.erb +4 -0
  110. data/app/views/cli/core/started.erb +11 -0
  111. data/app/views/cli/enumeration/backup_folders.erb +11 -0
  112. data/app/views/cli/enumeration/plugin.erb +13 -0
  113. data/app/views/cli/enumeration/plugins.erb +1 -12
  114. data/app/views/cli/enumeration/theme.erb +4 -0
  115. data/app/views/cli/enumeration/themes.erb +1 -3
  116. data/app/views/cli/enumeration/user.erb +4 -0
  117. data/app/views/cli/enumeration/users.erb +1 -3
  118. data/app/views/cli/finding.erb +1 -1
  119. data/app/views/cli/interesting_findings/_array.erb +10 -0
  120. data/app/views/cli/interesting_findings/findings.erb +23 -0
  121. data/app/views/cli/scan_aborted.erb +5 -0
  122. data/app/views/cli/update_aborted.erb +5 -0
  123. data/app/views/cli/vuln_api/status.erb +2 -0
  124. data/app/views/cli/vulnerability.erb +6 -0
  125. data/app/views/cli/wp_item.erb +4 -1
  126. data/app/views/json/core/banner.erb +2 -8
  127. data/app/views/json/core/finished.erb +13 -0
  128. data/app/views/json/core/help.erb +4 -0
  129. data/app/views/json/core/started.erb +10 -0
  130. data/app/views/json/enumeration/backup_folders.erb +11 -0
  131. data/app/views/json/enumeration/plugin.erb +15 -0
  132. data/app/views/json/enumeration/theme.erb +5 -0
  133. data/app/views/json/enumeration/user.erb +6 -0
  134. data/app/views/json/finding.erb +8 -2
  135. data/app/views/json/interesting_findings/findings.erb +24 -0
  136. data/app/views/json/notice.erb +1 -0
  137. data/app/views/json/scan_aborted.erb +5 -0
  138. data/app/views/json/update_aborted.erb +5 -0
  139. data/app/views/json/vuln_api/status.erb +2 -0
  140. data/app/views/json/wp_item.erb +4 -1
  141. data/bin/wpscan +1 -0
  142. data/lib/opt_parse_validator/config_files_loader_merger/base.rb +26 -0
  143. data/lib/opt_parse_validator/config_files_loader_merger/json.rb +17 -0
  144. data/lib/opt_parse_validator/config_files_loader_merger/yml.rb +17 -0
  145. data/lib/opt_parse_validator/config_files_loader_merger.rb +62 -0
  146. data/lib/opt_parse_validator/errors.rb +9 -0
  147. data/lib/opt_parse_validator/hacks.rb +19 -0
  148. data/lib/opt_parse_validator/opts/alias.rb +28 -0
  149. data/lib/opt_parse_validator/opts/array.rb +34 -0
  150. data/lib/opt_parse_validator/opts/base.rb +142 -0
  151. data/lib/opt_parse_validator/opts/boolean.rb +19 -0
  152. data/lib/opt_parse_validator/opts/choice.rb +43 -0
  153. data/lib/opt_parse_validator/opts/credentials.rb +15 -0
  154. data/lib/opt_parse_validator/opts/directory_path.rb +17 -0
  155. data/lib/opt_parse_validator/opts/file_path.rb +34 -0
  156. data/lib/opt_parse_validator/opts/headers.rb +33 -0
  157. data/lib/opt_parse_validator/opts/integer.rb +15 -0
  158. data/lib/opt_parse_validator/opts/integer_range.rb +37 -0
  159. data/lib/opt_parse_validator/opts/multi_choices.rb +135 -0
  160. data/lib/opt_parse_validator/opts/path.rb +78 -0
  161. data/lib/opt_parse_validator/opts/positive_integer.rb +16 -0
  162. data/lib/opt_parse_validator/opts/proxy.rb +7 -0
  163. data/lib/opt_parse_validator/opts/regexp.rb +14 -0
  164. data/lib/opt_parse_validator/opts/smart_list.rb +30 -0
  165. data/lib/opt_parse_validator/opts/string.rb +8 -0
  166. data/lib/opt_parse_validator/opts/uri.rb +41 -0
  167. data/lib/opt_parse_validator/opts/url.rb +11 -0
  168. data/lib/opt_parse_validator/opts.rb +9 -0
  169. data/lib/opt_parse_validator/version.rb +6 -0
  170. data/lib/opt_parse_validator.rb +161 -0
  171. data/lib/wpscan/browser/actions.rb +48 -0
  172. data/lib/wpscan/browser/options.rb +92 -0
  173. data/lib/wpscan/browser.rb +87 -2
  174. data/lib/wpscan/browser_authenticator.rb +64 -0
  175. data/lib/wpscan/cache/file_store.rb +77 -0
  176. data/lib/wpscan/cache/typhoeus.rb +25 -0
  177. data/lib/wpscan/controller.rb +100 -4
  178. data/lib/wpscan/controllers.rb +78 -3
  179. data/lib/wpscan/db/dynamic_finders/base.rb +3 -7
  180. data/lib/wpscan/db/dynamic_finders/plugin.rb +2 -2
  181. data/lib/wpscan/db/dynamic_finders/wordpress.rb +1 -1
  182. data/lib/wpscan/db/fingerprints.rb +2 -2
  183. data/lib/wpscan/db/updater.rb +23 -13
  184. data/lib/wpscan/db/vuln_api.rb +19 -7
  185. data/lib/wpscan/db/wp_item.rb +2 -2
  186. data/lib/wpscan/errors/enumeration.rb +4 -4
  187. data/lib/wpscan/errors/http.rb +82 -3
  188. data/lib/wpscan/errors/saml.rb +28 -0
  189. data/lib/wpscan/errors/scan.rb +14 -0
  190. data/lib/wpscan/errors/update.rb +11 -3
  191. data/lib/wpscan/errors/vuln_api.rb +24 -0
  192. data/lib/wpscan/errors/wordpress.rb +2 -2
  193. data/lib/wpscan/errors/wp_auth.rb +37 -0
  194. data/lib/wpscan/errors.rb +4 -3
  195. data/lib/wpscan/exit_code.rb +25 -0
  196. data/lib/wpscan/finders/base_finders.rb +45 -0
  197. data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -1
  198. data/lib/wpscan/finders/dynamic_finder/version/body_pattern.rb +1 -1
  199. data/lib/wpscan/finders/dynamic_finder/version/comment.rb +1 -1
  200. data/lib/wpscan/finders/dynamic_finder/version/header_pattern.rb +1 -1
  201. data/lib/wpscan/finders/dynamic_finder/version/javascript_var.rb +1 -1
  202. data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +3 -5
  203. data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
  204. data/lib/wpscan/finders/dynamic_finder/wp_items/finder.rb +3 -3
  205. data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
  206. data/lib/wpscan/finders/finder/breadth_first_dictionary_attack.rb +257 -0
  207. data/lib/wpscan/finders/finder/enumerator.rb +77 -0
  208. data/lib/wpscan/finders/finder/fingerprinter.rb +48 -0
  209. data/lib/wpscan/finders/finder/smart_url_checker/findings.rb +33 -0
  210. data/lib/wpscan/finders/finder/smart_url_checker.rb +60 -0
  211. data/lib/wpscan/finders/finder/wp_version/smart_url_checker.rb +1 -1
  212. data/lib/wpscan/finders/finder.rb +78 -0
  213. data/lib/wpscan/finders/finding.rb +54 -0
  214. data/lib/wpscan/finders/findings.rb +33 -0
  215. data/lib/wpscan/finders/independent_finder.rb +33 -0
  216. data/lib/wpscan/finders/independent_finders.rb +26 -0
  217. data/lib/wpscan/finders/same_type_finder.rb +19 -0
  218. data/lib/wpscan/finders/same_type_finders.rb +28 -0
  219. data/lib/wpscan/finders/unique_finder.rb +19 -0
  220. data/lib/wpscan/finders/unique_finders.rb +47 -0
  221. data/lib/wpscan/finders.rb +11 -12
  222. data/lib/wpscan/formatter/buffer.rb +17 -0
  223. data/lib/wpscan/formatter.rb +152 -0
  224. data/lib/wpscan/helper.rb +7 -1
  225. data/lib/wpscan/http_status_tracking.rb +128 -0
  226. data/lib/wpscan/numeric.rb +13 -0
  227. data/lib/wpscan/parsed_cli.rb +31 -2
  228. data/lib/wpscan/progressbar_null_output.rb +23 -0
  229. data/lib/wpscan/public_suffix/domain.rb +44 -0
  230. data/lib/wpscan/references.rb +118 -4
  231. data/lib/wpscan/scan.rb +127 -0
  232. data/lib/wpscan/target/hashes.rb +45 -0
  233. data/lib/wpscan/target/platform/php.rb +124 -0
  234. data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
  235. data/lib/wpscan/target/platform/wordpress.rb +7 -8
  236. data/lib/wpscan/target/platform.rb +3 -0
  237. data/lib/wpscan/target/scope.rb +103 -0
  238. data/lib/wpscan/target/server/apache.rb +27 -0
  239. data/lib/wpscan/target/server/generic.rb +72 -0
  240. data/lib/wpscan/target/server/iis.rb +29 -0
  241. data/lib/wpscan/target/server/nginx.rb +27 -0
  242. data/lib/wpscan/target/server.rb +6 -0
  243. data/lib/wpscan/target.rb +129 -9
  244. data/lib/wpscan/typhoeus/hydra.rb +12 -0
  245. data/lib/wpscan/typhoeus/response.rb +24 -1
  246. data/lib/wpscan/version.rb +1 -1
  247. data/lib/wpscan/vulnerability.rb +49 -3
  248. data/lib/wpscan/vulnerability_filter.rb +68 -0
  249. data/lib/wpscan/vulnerable.rb +13 -1
  250. data/lib/wpscan/web_site.rb +152 -0
  251. data/lib/wpscan.rb +126 -29
  252. metadata +362 -20
@@ -0,0 +1,11 @@
1
+ "backup_folders": {
2
+ <% unless @backup_folders.empty? -%>
3
+ <% last_index = @backup_folders.size - 1 -%>
4
+ <% @backup_folders.each_with_index do |backup_folder, index| -%>
5
+ <%= backup_folder.url.to_json %>: {
6
+ "severity": <%= backup_folder.severity.to_json %>,
7
+ <%= render('@finding', item: backup_folder) -%>
8
+ }<% unless index == last_index -%>,<% end -%>
9
+ <% end -%>
10
+ <% end -%>
11
+ },
@@ -0,0 +1,15 @@
1
+ "plugin": {
2
+ <%= @plugin.slug.to_json %>: {
3
+ <%= render('@wp_item', wp_item: @plugin) %>,
4
+ <%= render('@finding', item: @plugin) -%>,
5
+ <% if @plugin.version -%>
6
+ "version": {
7
+ "number": <%= @plugin.version.number.to_json %>,
8
+ "confidence": <%= @plugin.version.confidence.to_json %>,
9
+ <%= render('@finding', item: @plugin.version) -%>
10
+ }
11
+ <% else -%>
12
+ "version": null
13
+ <% end -%>
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ "theme": {
2
+ <%= @theme.slug.to_json %>: {
3
+ <%= render('@theme', theme: @theme) -%>
4
+ }
5
+ }
@@ -0,0 +1,6 @@
1
+ "user": {
2
+ <%= @user.username.to_json %>: {
3
+ "id": <%= @user.id.to_json %>,
4
+ <%= render('@finding', item: @user) -%>
5
+ }
6
+ }
@@ -12,17 +12,23 @@
12
12
  <% end -%>
13
13
  <% end -%>
14
14
  }
15
- <% if @item.respond_to?(:vulnerabilities) -%>
15
+ <% if @item.respond_to?(:filtered_vulnerabilities) -%>
16
16
  ,"vulnerabilities": [
17
- <% unless (vulns = @item.vulnerabilities).empty? -%>
17
+ <% unless (vulns = @item.filtered_vulnerabilities).empty? -%>
18
18
  <% last_index = vulns.size - 1 -%>
19
19
  <% vulns.each_with_index do |v, index| -%>
20
20
  {
21
21
  "title": <%= v.title.to_json %>,
22
+ <% if v.uuid -%>
23
+ "uuid": <%= v.uuid.to_json %>,
24
+ <% end -%>
22
25
  <% if v.cvss -%>
23
26
  "cvss": <%= v.cvss.to_json %>,
24
27
  <% end -%>
25
28
  "fixed_in": <%= v.fixed_in.to_json %>,
29
+ <% if v.poc -%>
30
+ "poc": <%= v.poc.to_json %>,
31
+ <% end -%>
26
32
  "references": <%= v.references.to_json %>
27
33
  }<% unless index == last_index -%>,<% end -%>
28
34
  <% end -%>
@@ -0,0 +1,24 @@
1
+ "interesting_findings": [
2
+ <% unless @findings.empty? -%>
3
+ <% last_index = @findings.size - 1 %>
4
+ <% @findings.each.with_index do |finding, index| -%>
5
+ {
6
+ "url": <%= finding.url.to_s.to_json %>,
7
+ "to_s": <%= finding.to_s.to_json %>,
8
+ "type": <%= finding.type.to_json %>,
9
+ "found_by": <%= finding.found_by.to_s.to_json %>,
10
+ "confidence": <%= finding.confidence.to_json %>,
11
+ "confirmed_by": {
12
+ <% unless (confirmed = finding.confirmed_by).empty? -%>
13
+ <% c_last_index = confirmed.size - 1 %>
14
+ <% confirmed.each.with_index do |c, i| -%>
15
+ <%= c.found_by.to_s.to_json %>: { "confidence": <%= c.confidence.to_json %> }<% unless i == c_last_index %>,<% end %>
16
+ <% end -%>
17
+ <% end -%>
18
+ },
19
+ "references": <%= finding.references.to_json %>,
20
+ "interesting_entries": <%= finding.interesting_entries.to_json %>
21
+ }<% unless index == last_index %>,<% end %>
22
+ <% end -%>
23
+ <% end -%>
24
+ ],
@@ -0,0 +1 @@
1
+ "notice": <%= @msg.to_json %>,
@@ -0,0 +1,5 @@
1
+ "scan_aborted": <%= @reason.to_json %>,
2
+ "target_url": <%= @url.to_json %>,
3
+ <% if @verbose -%>
4
+ "trace": <%= @trace.to_json %>,
5
+ <% end %>
@@ -0,0 +1,5 @@
1
+ "update_aborted": <%= @reason.to_json %>,
2
+ "target_url": <%= @url.to_json %>,
3
+ <% if @verbose -%>
4
+ "trace": <%= @trace.to_json %>,
5
+ <% end %>
@@ -2,6 +2,8 @@
2
2
  <% unless @status.empty? -%>
3
3
  <% if @status['http_error'] -%>
4
4
  "http_error": <%= @status['http_error'].to_s.to_json %>
5
+ <% elsif @status['parse_error'] -%>
6
+ "parse_error": "WPScan DB API returned an invalid response. Please check your network/proxy configuration or try again later."
5
7
  <% else -%>
6
8
  "plan": <%= @status['plan'].to_json %>,
7
9
  "requests_done_during_scan": <%= @api_requests.to_json %>,
@@ -1,7 +1,10 @@
1
1
  "slug": <%= @wp_item.slug.to_json %>,
2
2
  "location": <%= @wp_item.url.to_json %>,
3
3
  "latest_version": <%= @wp_item.latest_version ? @wp_item.latest_version.number.to_json : nil.to_json %>,
4
- "last_updated": <%= @wp_item.last_updated.to_json %>,
4
+ "last_updated": <%= @wp_item.last_updated_iso.to_json %>,
5
+ "last_updated_relative": <%= @wp_item.last_updated_relative.to_json %>,
6
+ "last_updated_source": <%= @wp_item.last_updated_source.to_json %>,
7
+ "active_installs": <%= @wp_item.active_installs.to_json %>,
5
8
  "outdated": <%= @wp_item.outdated?.to_json %>,
6
9
  "readme_url": <%= @wp_item.readme_url.to_json %>,
7
10
  "directory_listing": <%= @wp_item.directory_listing?.to_json %>,
data/bin/wpscan CHANGED
@@ -9,6 +9,7 @@ WPScan::Scan.new do |s|
9
9
  WPScan::Controller::CustomDirectories.new <<
10
10
  WPScan::Controller::InterestingFindings.new <<
11
11
  WPScan::Controller::WpVersion.new <<
12
+ WPScan::Controller::AuthenticatedInventory.new <<
12
13
  WPScan::Controller::MainTheme.new <<
13
14
  WPScan::Controller::Enumeration.new <<
14
15
  WPScan::Controller::PasswordAttack.new <<
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ class ConfigFilesLoaderMerger < Array
5
+ module ConfigFile
6
+ # Base class, #parse should be implemented in child classes
7
+ class Base
8
+ attr_reader :path
9
+
10
+ # @param [ String ] path The file path of the option file
11
+ def initialize(path)
12
+ @path = path
13
+ end
14
+
15
+ # @return [ Hash ] a { 'key' => value } hash
16
+ def parse
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def ==(other)
21
+ path == other.path
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module OptParseValidator
6
+ class ConfigFilesLoaderMerger < Array
7
+ module ConfigFile
8
+ # Json Implementation
9
+ class JSON < Base
10
+ # @return [ Hash ] a { 'key' => value } hash
11
+ def parse
12
+ ::JSON.parse(File.read(path))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module OptParseValidator
6
+ class ConfigFilesLoaderMerger < Array
7
+ module ConfigFile
8
+ # Yaml Implementation
9
+ class YML < Base
10
+ # @return [ Hash ] a { 'key' => value } hash
11
+ def parse
12
+ yaml_safe_load(path)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config_files_loader_merger/base'
4
+ require_relative 'config_files_loader_merger/json'
5
+ require_relative 'config_files_loader_merger/yml'
6
+
7
+ # :nocov:
8
+ # @param [ String ] path The path of the file to load
9
+ def yaml_safe_load(path)
10
+ if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1') # Ruby 2.6
11
+ YAML.safe_load_file(path, permitted_classes: [Regexp]) || {}
12
+ else
13
+ YAML.safe_load_file(path, [Regexp]) || {}
14
+ end
15
+ end
16
+ # :nocov:
17
+
18
+ module OptParseValidator
19
+ # Options Files container
20
+ class ConfigFilesLoaderMerger < Array
21
+ # @return [ Array<String> ] The downcased supported extensions list
22
+ def self.supported_extensions
23
+ extensions = ConfigFile.constants.select do |const|
24
+ name = ConfigFile.const_get(const)
25
+ name.is_a?(Class) && name != ConfigFile::Base
26
+ end
27
+
28
+ extensions.map { |sym| sym.to_s.downcase }
29
+ end
30
+
31
+ attr_accessor :result_key
32
+
33
+ # @param [ String ] file_path
34
+ #
35
+ # @return [ Self ]
36
+ def <<(file_path)
37
+ return self unless File.exist?(file_path)
38
+
39
+ ext = File.extname(file_path).delete('.')
40
+
41
+ unless self.class.supported_extensions.include?(ext)
42
+ raise Error,
43
+ "The option file's extension '#{ext}' is not supported"
44
+ end
45
+
46
+ super(ConfigFile.const_get(ext.upcase).new(file_path))
47
+ end
48
+
49
+ # @params [ Hash ] opts
50
+ #
51
+ # @return [ Hash ]
52
+ def parse
53
+ result = {}
54
+
55
+ each { |config_file| result.deep_merge!(config_file.parse) }
56
+
57
+ result = result[result_key] || {} if result_key
58
+
59
+ result.deep_symbolize_keys
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ class Error < RuntimeError
5
+ end
6
+
7
+ class NoRequiredOption < Error
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OptionParser
4
+ # Hack to suppress the completion (except for the -h/--help) which was leading to
5
+ # unwanted behaviours
6
+ # See https://github.com/wpscanteam/CMSScanner/issues/2
7
+ module Completion
8
+ class << self
9
+ alias original_candidate candidate
10
+
11
+ # rubocop:disable Style/OptionalBooleanParameter
12
+ def candidate(key, icase = false, pat = nil, &)
13
+ # Maybe also do this for -v/--version ?
14
+ key == 'h' ? original_candidate('help', icase, pat, &) : []
15
+ end
16
+ # rubocop:enable Style/OptionalBooleanParameter
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ # Implementation of the Alias Option
5
+ class OptAlias < OptBase
6
+ def initialize(option, attrs = {})
7
+ raise Error, 'The :alias_for attribute is required' unless attrs.key?(:alias_for)
8
+
9
+ super
10
+ end
11
+
12
+ def append_help_messages
13
+ super
14
+
15
+ option << "Alias for #{alias_for}"
16
+ end
17
+
18
+ # @return [ String ]
19
+ def alias_for
20
+ @alias_for ||= attrs[:alias_for]
21
+ end
22
+
23
+ # @return [ Boolean ]
24
+ def alias?
25
+ true
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ # Implementation of the Array Option
5
+ class OptArray < OptBase
6
+ # @return [ Void ]
7
+ def append_help_messages
8
+ option << "Separator to use between the values: '#{separator}'"
9
+
10
+ super
11
+ end
12
+
13
+ # @param [ String ] value
14
+ #
15
+ # @return [ Array ]
16
+ def validate(value)
17
+ super.split(separator)
18
+ end
19
+
20
+ # @return [ String ] The separator used to split the string into an array
21
+ def separator
22
+ attrs[:separator] || ','
23
+ end
24
+
25
+ # See OptBase#normalize
26
+ # @param [ Array ] values
27
+ def normalize(values)
28
+ values.each_with_index do |value, index|
29
+ values[index] = super(value)
30
+ end
31
+ values
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ # Base Option
5
+ # This Option should not be called, children should be used.
6
+ class OptBase
7
+ attr_writer :required
8
+ attr_reader :option, :attrs
9
+
10
+ # @param [ Array ] option See OptionParser#on
11
+ # @param [ Hash ] attrs
12
+ # @option attrs [ Boolean ] :required
13
+ # @options attrs [ Array<Symbol>, Symbol ] :required_unless
14
+ # @option attrs [ Mixed ] :default The default value to use if the option is not supplied
15
+ # @option attrs [ Mixed ] :value_if_empty The value to use if no argument has been supplied
16
+ # @option attrs [ Array<Symbol> ] :normalize See #normalize
17
+ #
18
+ # @note The :default and :normalize 'logics' are done in OptParseValidator::OptParser#add_option
19
+ def initialize(option, attrs = {})
20
+ @option = option
21
+ @attrs = attrs
22
+
23
+ # TODO: incompatible attributes, ie required and require_unless at the same time
24
+
25
+ append_help_messages
26
+ end
27
+
28
+ # @return [ Void ]
29
+ def append_help_messages
30
+ option << "Default: #{help_message_for_default}" if default
31
+ option << "Value if no argument supplied: #{value_if_empty}" if value_if_empty
32
+ option << 'This option is mandatory' if required?
33
+ return if required_unless.empty?
34
+
35
+ option << "This option is mandatory unless #{required_unless.join(' or ')} is/are supplied"
36
+ end
37
+
38
+ def help_message_for_default
39
+ default.to_s
40
+ end
41
+
42
+ # @return [ Boolean ]
43
+ def required?
44
+ @required ||= attrs[:required]
45
+ end
46
+
47
+ def required_unless
48
+ @required_unless ||= Array(attrs[:required_unless])
49
+ end
50
+
51
+ # @return [ Mixed ]
52
+ def default
53
+ attrs[:default]
54
+ end
55
+
56
+ # @return [ Array<Mixed> ]
57
+ def choices
58
+ attrs[:choices]
59
+ end
60
+
61
+ # @return [ Mixed ]
62
+ def value_if_empty
63
+ attrs[:value_if_empty]
64
+ end
65
+
66
+ # @return [ Boolean ]
67
+ def alias?
68
+ false
69
+ end
70
+
71
+ # @return [ Boolean ]
72
+ def advanced?
73
+ attrs[:advanced] ? true : false
74
+ end
75
+
76
+ # @param [ String ] value
77
+ def validate(value)
78
+ if value.nil? || value.to_s.empty?
79
+ raise Error, 'Empty option value supplied' if value_if_empty.nil?
80
+
81
+ return value_if_empty
82
+ end
83
+ value
84
+ end
85
+
86
+ # Apply each methods from attrs[:normalize] to the value if possible
87
+ # User input should not be used in this attrs[:normalize]
88
+ #
89
+ # e.g: normalize: :to_sym will return the symbol of the value
90
+ # normalize: [:to_sym, :upcase] Will return the upercased symbol
91
+ #
92
+ # @param [ Mixed ] value
93
+ #
94
+ # @return [ Mixed ]
95
+ def normalize(value)
96
+ Array(attrs[:normalize]).each do |method|
97
+ next unless method.is_a?(Symbol)
98
+
99
+ value = value.send(method) if value.respond_to?(method)
100
+ end
101
+
102
+ value
103
+ end
104
+
105
+ # @return [ Symbol ]
106
+ def to_sym
107
+ unless @symbol
108
+ long_option = to_long
109
+
110
+ raise Error, "Could not find option symbol for #{option}" unless long_option
111
+
112
+ @symbol = long_option.delete_prefix('--').tr('-', '_').to_sym
113
+ end
114
+ @symbol
115
+ end
116
+
117
+ # @return [ String ] The raw long option (e.g: --proxy)
118
+ def to_long
119
+ option.each do |option_attr|
120
+ if option_attr.start_with?('--')
121
+ return option_attr.gsub(/ .*$/, '')
122
+ .gsub(/\[[^\]]+\]/, '')
123
+ end
124
+ end
125
+ nil
126
+ end
127
+
128
+ # @return [ String ]
129
+ def to_s
130
+ to_sym.to_s
131
+ end
132
+
133
+ # @return [ Array<String> ]
134
+ def help_messages
135
+ first_message_index = option.index { |e| e[0] != '-' }
136
+
137
+ return [] unless first_message_index
138
+
139
+ option[first_message_index..]
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ # Implementation of the Boolean Option
5
+ class OptBoolean < OptBase
6
+ TRUE_PATTERN = /\A(true|t|yes|y|1)\z/i
7
+ FALSE_PATTERN = /\A(false|f|no|n|0)\z/i
8
+
9
+ # @return [ Boolean ]
10
+ def validate(value)
11
+ value = value.to_s
12
+
13
+ return true if value.match(TRUE_PATTERN)
14
+ return false if value.match(FALSE_PATTERN)
15
+
16
+ raise Error, 'Invalid boolean value, expected true|t|yes|y|1|false|f|no|n|0'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ # Implementation of the Choice Option
5
+ class OptChoice < OptBase
6
+ # @param [ Array ] option See OptBase#new
7
+ # @param [ Hash ] attrs
8
+ # :choices [ Array ] The available choices (mandatory)
9
+ # :case_sensitive [ Boolean ] Default: false
10
+ def initialize(option, attrs = {})
11
+ raise Error, 'The :choices attribute is mandatory' unless attrs.key?(:choices)
12
+ raise Error, 'The :choices attribute must be an array' unless attrs[:choices].is_a?(Array)
13
+
14
+ super
15
+ end
16
+
17
+ # @return [ Void ]
18
+ def append_help_messages
19
+ super
20
+
21
+ option << "Available choices: #{choices.join(', ')}"
22
+ end
23
+
24
+ # @return [ String ]
25
+ # If :case_sensitive if false (or nil), the downcased value of the choice
26
+ # will be returned
27
+ def validate(value)
28
+ value = +value.to_s
29
+
30
+ unless attrs[:case_sensitive]
31
+ value.downcase!
32
+ choices.map!(&:downcase)
33
+ end
34
+
35
+ unless choices.include?(value)
36
+ raise Error, "'#{value}' is not a valid choice, expected one " \
37
+ "of the followings: #{choices.join(',')}"
38
+ end
39
+
40
+ value
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ # Implementation of the Credentials Option
5
+ class OptCredentials < OptBase
6
+ # @return [ Hash ] A hash containing the :username and :password
7
+ def validate(value)
8
+ raise Error, 'Incorrect credentials format, username:password expected' unless value.index(':')
9
+
10
+ creds = value.split(':', 2)
11
+
12
+ { username: creds[0], password: creds[1] }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ # Implemetantion of the DirectoryPath Option
5
+ class OptDirectoryPath < OptPath
6
+ def initialize(option, attrs = {})
7
+ super
8
+
9
+ @attrs.merge!(directory: true)
10
+ end
11
+
12
+ # @param [ Pathname ] path
13
+ def check_create(path)
14
+ FileUtils.mkdir_p(path.to_s)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ # Implementation of the FilePath Option
5
+ class OptFilePath < OptPath
6
+ # @param [ Array ] option See OptBase#new
7
+ # @param [ Hash ] attrs See OptPath#new
8
+ # :extensions [ Array | String ] The allowed extension(s)
9
+ def initialize(option, attrs = {})
10
+ super
11
+
12
+ @attrs.merge!(file: true)
13
+ end
14
+
15
+ # @param [ Pathname ] path
16
+ def check_create(path)
17
+ return if File.exist?(path.to_s)
18
+
19
+ FileUtils.mkdir_p(path.parent.to_s)
20
+ FileUtils.touch(path.to_s)
21
+ end
22
+
23
+ def allowed_attrs
24
+ # :extensions is put at the first place
25
+ [:extensions] + super
26
+ end
27
+
28
+ def check_extensions(path)
29
+ return if Array(attrs[:extensions]).include?(path.extname.delete('.'))
30
+
31
+ raise Error, "The extension of '#{path}' is not allowed"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptParseValidator
4
+ # Implementation of the Headers Option
5
+ class OptHeaders < OptBase
6
+ # @return [ Void ]
7
+ def append_help_messages
8
+ super
9
+
10
+ option << "Separator to use between the headers: '; '"
11
+ option << "Examples: 'X-Forwarded-For: 127.0.0.1', 'X-Forwarded-For: 127.0.0.1; Another: aaa'"
12
+ end
13
+
14
+ # @param [ String ] value
15
+ #
16
+ # @return [ Hash ] The parsed headers in a hash, with { 'key' => 'value' } format
17
+ def validate(value)
18
+ values = super.chomp(';').split('; ')
19
+
20
+ headers = {}
21
+
22
+ values.each do |header|
23
+ raise Error, "Malformed header: '#{header}'" unless header.index(':')
24
+
25
+ val = header.split(':', 2)
26
+
27
+ headers[val[0].strip] = val[1].strip
28
+ end
29
+
30
+ headers
31
+ end
32
+ end
33
+ end