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.
- checksums.yaml +4 -4
- data/README.md +104 -30
- data/app/app.rb +26 -0
- data/app/controllers/aliases.rb +2 -2
- data/app/controllers/authenticated_inventory.rb +43 -0
- data/app/controllers/core/cli_options.rb +151 -0
- data/app/controllers/core.rb +200 -25
- data/app/controllers/custom_directories.rb +1 -1
- data/app/controllers/enumeration/cli_options.rb +21 -31
- data/app/controllers/enumeration/enum_methods.rb +145 -38
- data/app/controllers/enumeration.rb +26 -3
- data/app/controllers/interesting_findings.rb +25 -0
- data/app/controllers/main_theme.rb +1 -1
- data/app/controllers/password_attack.rb +14 -6
- data/app/controllers/vuln_api.rb +9 -3
- data/app/controllers/wp_version.rb +1 -1
- data/app/controllers.rb +1 -0
- data/app/finders/backup_folders/known_locations.rb +66 -0
- data/app/finders/backup_folders.rb +19 -0
- data/app/finders/config_backups/known_filenames.rb +6 -4
- data/app/finders/config_backups.rb +1 -1
- data/app/finders/db_exports/known_locations.rb +16 -14
- data/app/finders/db_exports.rb +1 -1
- data/app/finders/interesting_findings/backup_db.rb +1 -1
- data/app/finders/interesting_findings/debug_log.rb +1 -1
- data/app/finders/interesting_findings/duplicator_installer_log.rb +1 -1
- data/app/finders/interesting_findings/emergency_pwd_reset_script.rb +1 -1
- data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
- data/app/finders/interesting_findings/full_path_disclosure.rb +1 -1
- data/app/finders/interesting_findings/headers.rb +17 -0
- data/app/finders/interesting_findings/mu_plugins.rb +1 -1
- data/app/finders/interesting_findings/multisite.rb +1 -1
- data/app/finders/interesting_findings/php_disabled.rb +2 -2
- data/app/finders/interesting_findings/readme.rb +1 -1
- data/app/finders/interesting_findings/registration.rb +1 -1
- data/app/finders/interesting_findings/robots_txt.rb +20 -0
- data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
- data/app/finders/interesting_findings/tmm_db_migrate.rb +1 -1
- data/app/finders/interesting_findings/upload_directory_listing.rb +1 -1
- data/app/finders/interesting_findings/upload_sql_dump.rb +2 -2
- data/app/finders/interesting_findings/wp_cron.rb +1 -1
- data/app/finders/interesting_findings/xml_rpc.rb +61 -0
- data/app/finders/interesting_findings.rb +13 -4
- data/app/finders/main_theme/css_style_in_homepage.rb +1 -1
- data/app/finders/main_theme/urls_in_homepage.rb +3 -7
- data/app/finders/main_theme/woo_framework_meta_generator.rb +4 -4
- data/app/finders/main_theme.rb +1 -1
- data/app/finders/medias/attachment_brute_forcing.rb +2 -2
- data/app/finders/medias.rb +1 -1
- data/app/finders/passwords/wp_login.rb +2 -2
- data/app/finders/passwords/xml_rpc.rb +2 -2
- data/app/finders/passwords/xml_rpc_multicall.rb +1 -1
- data/app/finders/plugin_version/readme.rb +1 -1
- data/app/finders/plugin_version.rb +1 -1
- data/app/finders/plugins/known_locations.rb +17 -7
- data/app/finders/plugins/urls_in_homepage.rb +3 -7
- data/app/finders/plugins/wp_json_api.rb +85 -0
- data/app/finders/plugins.rb +2 -1
- data/app/finders/theme_version/style.rb +1 -1
- data/app/finders/theme_version/woo_framework_meta_generator.rb +1 -1
- data/app/finders/theme_version.rb +1 -1
- data/app/finders/themes/known_locations.rb +12 -6
- data/app/finders/themes/urls_in_homepage.rb +3 -7
- data/app/finders/themes/wp_json_api.rb +74 -0
- data/app/finders/themes.rb +2 -1
- data/app/finders/timthumb_version/bad_request.rb +1 -1
- data/app/finders/timthumb_version.rb +1 -1
- data/app/finders/timthumbs/known_locations.rb +6 -4
- data/app/finders/timthumbs.rb +1 -1
- data/app/finders/users/author_id_brute_forcing.rb +11 -7
- data/app/finders/users/author_posts.rb +1 -1
- data/app/finders/users/author_sitemap.rb +1 -1
- data/app/finders/users/login_error_messages.rb +1 -1
- data/app/finders/users/oembed_api.rb +3 -1
- data/app/finders/users/wp_json_api.rb +11 -7
- data/app/finders/users.rb +1 -1
- data/app/finders/wp_version/atom_generator.rb +1 -1
- data/app/finders/wp_version/rdf_generator.rb +1 -1
- data/app/finders/wp_version/readme.rb +1 -1
- data/app/finders/wp_version/rss_generator.rb +1 -1
- data/app/finders/wp_version/unique_fingerprinting.rb +2 -2
- data/app/finders/wp_version.rb +1 -1
- data/app/finders.rb +1 -0
- data/app/formatters/cli.rb +79 -0
- data/app/formatters/cli_no_color.rb +9 -0
- data/app/formatters/cli_no_colour.rb +17 -0
- data/app/formatters/json.rb +14 -0
- data/app/formatters/jsonl.rb +29 -0
- data/app/formatters/sarif.rb +311 -0
- data/app/models/backup_folder.rb +39 -0
- data/app/models/fantastico_fileslist.rb +34 -0
- data/app/models/headers.rb +44 -0
- data/app/models/interesting_finding.rb +41 -2
- data/app/models/plugin.rb +8 -2
- data/app/models/robots_txt.rb +31 -0
- data/app/models/search_replace_db_2.rb +17 -0
- data/app/models/theme.rb +9 -2
- data/app/models/timthumb.rb +2 -2
- data/app/models/user.rb +35 -0
- data/app/models/version.rb +49 -0
- data/app/models/wp_item/wordpress_org_data.rb +55 -0
- data/app/models/wp_item.rb +109 -9
- data/app/models/wp_version.rb +2 -2
- data/app/models/xml_rpc.rb +73 -3
- data/app/models.rb +2 -1
- data/app/user_agents.txt +46 -0
- data/app/views/cli/core/banner.erb +3 -3
- data/app/views/cli/core/finished.erb +15 -0
- data/app/views/cli/core/help.erb +4 -0
- data/app/views/cli/core/started.erb +11 -0
- data/app/views/cli/enumeration/backup_folders.erb +11 -0
- data/app/views/cli/enumeration/plugin.erb +13 -0
- data/app/views/cli/enumeration/plugins.erb +1 -12
- data/app/views/cli/enumeration/theme.erb +4 -0
- data/app/views/cli/enumeration/themes.erb +1 -3
- data/app/views/cli/enumeration/user.erb +4 -0
- data/app/views/cli/enumeration/users.erb +1 -3
- data/app/views/cli/finding.erb +1 -1
- data/app/views/cli/interesting_findings/_array.erb +10 -0
- data/app/views/cli/interesting_findings/findings.erb +23 -0
- data/app/views/cli/scan_aborted.erb +5 -0
- data/app/views/cli/update_aborted.erb +5 -0
- data/app/views/cli/vuln_api/status.erb +2 -0
- data/app/views/cli/vulnerability.erb +6 -0
- data/app/views/cli/wp_item.erb +4 -1
- data/app/views/json/core/banner.erb +2 -8
- data/app/views/json/core/finished.erb +13 -0
- data/app/views/json/core/help.erb +4 -0
- data/app/views/json/core/started.erb +10 -0
- data/app/views/json/enumeration/backup_folders.erb +11 -0
- data/app/views/json/enumeration/plugin.erb +15 -0
- data/app/views/json/enumeration/theme.erb +5 -0
- data/app/views/json/enumeration/user.erb +6 -0
- data/app/views/json/finding.erb +8 -2
- data/app/views/json/interesting_findings/findings.erb +24 -0
- data/app/views/json/notice.erb +1 -0
- data/app/views/json/scan_aborted.erb +5 -0
- data/app/views/json/update_aborted.erb +5 -0
- data/app/views/json/vuln_api/status.erb +2 -0
- data/app/views/json/wp_item.erb +4 -1
- data/bin/wpscan +1 -0
- data/lib/opt_parse_validator/config_files_loader_merger/base.rb +26 -0
- data/lib/opt_parse_validator/config_files_loader_merger/json.rb +17 -0
- data/lib/opt_parse_validator/config_files_loader_merger/yml.rb +17 -0
- data/lib/opt_parse_validator/config_files_loader_merger.rb +62 -0
- data/lib/opt_parse_validator/errors.rb +9 -0
- data/lib/opt_parse_validator/hacks.rb +19 -0
- data/lib/opt_parse_validator/opts/alias.rb +28 -0
- data/lib/opt_parse_validator/opts/array.rb +34 -0
- data/lib/opt_parse_validator/opts/base.rb +142 -0
- data/lib/opt_parse_validator/opts/boolean.rb +19 -0
- data/lib/opt_parse_validator/opts/choice.rb +43 -0
- data/lib/opt_parse_validator/opts/credentials.rb +15 -0
- data/lib/opt_parse_validator/opts/directory_path.rb +17 -0
- data/lib/opt_parse_validator/opts/file_path.rb +34 -0
- data/lib/opt_parse_validator/opts/headers.rb +33 -0
- data/lib/opt_parse_validator/opts/integer.rb +15 -0
- data/lib/opt_parse_validator/opts/integer_range.rb +37 -0
- data/lib/opt_parse_validator/opts/multi_choices.rb +135 -0
- data/lib/opt_parse_validator/opts/path.rb +78 -0
- data/lib/opt_parse_validator/opts/positive_integer.rb +16 -0
- data/lib/opt_parse_validator/opts/proxy.rb +7 -0
- data/lib/opt_parse_validator/opts/regexp.rb +14 -0
- data/lib/opt_parse_validator/opts/smart_list.rb +30 -0
- data/lib/opt_parse_validator/opts/string.rb +8 -0
- data/lib/opt_parse_validator/opts/uri.rb +41 -0
- data/lib/opt_parse_validator/opts/url.rb +11 -0
- data/lib/opt_parse_validator/opts.rb +9 -0
- data/lib/opt_parse_validator/version.rb +6 -0
- data/lib/opt_parse_validator.rb +161 -0
- data/lib/wpscan/browser/actions.rb +48 -0
- data/lib/wpscan/browser/options.rb +92 -0
- data/lib/wpscan/browser.rb +87 -2
- data/lib/wpscan/browser_authenticator.rb +64 -0
- data/lib/wpscan/cache/file_store.rb +77 -0
- data/lib/wpscan/cache/typhoeus.rb +25 -0
- data/lib/wpscan/controller.rb +100 -4
- data/lib/wpscan/controllers.rb +78 -3
- data/lib/wpscan/db/dynamic_finders/base.rb +3 -7
- data/lib/wpscan/db/dynamic_finders/plugin.rb +2 -2
- data/lib/wpscan/db/dynamic_finders/wordpress.rb +1 -1
- data/lib/wpscan/db/fingerprints.rb +2 -2
- data/lib/wpscan/db/updater.rb +23 -13
- data/lib/wpscan/db/vuln_api.rb +19 -7
- data/lib/wpscan/db/wp_item.rb +2 -2
- data/lib/wpscan/errors/enumeration.rb +4 -4
- data/lib/wpscan/errors/http.rb +82 -3
- data/lib/wpscan/errors/saml.rb +28 -0
- data/lib/wpscan/errors/scan.rb +14 -0
- data/lib/wpscan/errors/update.rb +11 -3
- data/lib/wpscan/errors/vuln_api.rb +24 -0
- data/lib/wpscan/errors/wordpress.rb +2 -2
- data/lib/wpscan/errors/wp_auth.rb +37 -0
- data/lib/wpscan/errors.rb +4 -3
- data/lib/wpscan/exit_code.rb +25 -0
- data/lib/wpscan/finders/base_finders.rb +45 -0
- data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/body_pattern.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/comment.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/header_pattern.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/javascript_var.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +3 -5
- data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/wp_items/finder.rb +3 -3
- data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
- data/lib/wpscan/finders/finder/breadth_first_dictionary_attack.rb +257 -0
- data/lib/wpscan/finders/finder/enumerator.rb +77 -0
- data/lib/wpscan/finders/finder/fingerprinter.rb +48 -0
- data/lib/wpscan/finders/finder/smart_url_checker/findings.rb +33 -0
- data/lib/wpscan/finders/finder/smart_url_checker.rb +60 -0
- data/lib/wpscan/finders/finder/wp_version/smart_url_checker.rb +1 -1
- data/lib/wpscan/finders/finder.rb +78 -0
- data/lib/wpscan/finders/finding.rb +54 -0
- data/lib/wpscan/finders/findings.rb +33 -0
- data/lib/wpscan/finders/independent_finder.rb +33 -0
- data/lib/wpscan/finders/independent_finders.rb +26 -0
- data/lib/wpscan/finders/same_type_finder.rb +19 -0
- data/lib/wpscan/finders/same_type_finders.rb +28 -0
- data/lib/wpscan/finders/unique_finder.rb +19 -0
- data/lib/wpscan/finders/unique_finders.rb +47 -0
- data/lib/wpscan/finders.rb +11 -12
- data/lib/wpscan/formatter/buffer.rb +17 -0
- data/lib/wpscan/formatter.rb +152 -0
- data/lib/wpscan/helper.rb +7 -1
- data/lib/wpscan/http_status_tracking.rb +128 -0
- data/lib/wpscan/numeric.rb +13 -0
- data/lib/wpscan/parsed_cli.rb +31 -2
- data/lib/wpscan/progressbar_null_output.rb +23 -0
- data/lib/wpscan/public_suffix/domain.rb +44 -0
- data/lib/wpscan/references.rb +118 -4
- data/lib/wpscan/scan.rb +127 -0
- data/lib/wpscan/target/hashes.rb +45 -0
- data/lib/wpscan/target/platform/php.rb +124 -0
- data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
- data/lib/wpscan/target/platform/wordpress.rb +7 -8
- data/lib/wpscan/target/platform.rb +3 -0
- data/lib/wpscan/target/scope.rb +103 -0
- data/lib/wpscan/target/server/apache.rb +27 -0
- data/lib/wpscan/target/server/generic.rb +72 -0
- data/lib/wpscan/target/server/iis.rb +29 -0
- data/lib/wpscan/target/server/nginx.rb +27 -0
- data/lib/wpscan/target/server.rb +6 -0
- data/lib/wpscan/target.rb +129 -9
- data/lib/wpscan/typhoeus/hydra.rb +12 -0
- data/lib/wpscan/typhoeus/response.rb +24 -1
- data/lib/wpscan/version.rb +1 -1
- data/lib/wpscan/vulnerability.rb +49 -3
- data/lib/wpscan/vulnerability_filter.rb +68 -0
- data/lib/wpscan/vulnerable.rb +13 -1
- data/lib/wpscan/web_site.rb +152 -0
- data/lib/wpscan.rb +126 -29
- 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
|
+
}
|
data/app/views/json/finding.erb
CHANGED
|
@@ -12,17 +12,23 @@
|
|
|
12
12
|
<% end -%>
|
|
13
13
|
<% end -%>
|
|
14
14
|
}
|
|
15
|
-
<% if @item.respond_to?(:
|
|
15
|
+
<% if @item.respond_to?(:filtered_vulnerabilities) -%>
|
|
16
16
|
,"vulnerabilities": [
|
|
17
|
-
<% unless (vulns = @item.
|
|
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 %>,
|
|
@@ -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 %>,
|
data/app/views/json/wp_item.erb
CHANGED
|
@@ -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.
|
|
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,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
|