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,311 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module WPScan
|
|
6
|
+
module Formatter
|
|
7
|
+
# SARIF v2.1.0 Formatter.
|
|
8
|
+
#
|
|
9
|
+
# Emits scan results in SARIF v2.1.0 format so they can be consumed by
|
|
10
|
+
# static-analysis aggregators such as GitHub Code Scanning. WPScan is a
|
|
11
|
+
# DAST tool, so findings don't have a source file + line; we follow the
|
|
12
|
+
# mapping discussed on issue #1879:
|
|
13
|
+
#
|
|
14
|
+
# * `result.locations[].physicalLocation.artifactLocation.uri` carries
|
|
15
|
+
# the URL where the finding was observed. SARIF explicitly allows
|
|
16
|
+
# `uri` to be an absolute URL — see SARIF v2.1.0 §3.4.3
|
|
17
|
+
# (https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#_Toc34317419).
|
|
18
|
+
# * `result.locations[].logicalLocations[]` carries the WordPress
|
|
19
|
+
# component identity (core / plugin <slug> / theme <slug>) decoupled
|
|
20
|
+
# from any URL — see SARIF v2.1.0 §3.33
|
|
21
|
+
# (https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#_Toc34317719).
|
|
22
|
+
#
|
|
23
|
+
# GitHub's guidance on which SARIF fields surface in Code Scanning is
|
|
24
|
+
# at https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning.
|
|
25
|
+
#
|
|
26
|
+
# This formatter inherits from {Json} and reuses the JSON ERB views: the
|
|
27
|
+
# full scan is buffered as JSON during the run, then transformed into a
|
|
28
|
+
# SARIF document in {#beautify}. Reusing the JSON layer keeps the SARIF
|
|
29
|
+
# mapping in one place and lets new JSON fields flow through automatically.
|
|
30
|
+
class Sarif < Json
|
|
31
|
+
SARIF_VERSION = '2.1.0'
|
|
32
|
+
SARIF_SCHEMA = 'https://json.schemastore.org/sarif-2.1.0.json'
|
|
33
|
+
INFO_URI = 'https://wpscan.com/wordpress-security-scanner'
|
|
34
|
+
|
|
35
|
+
# Make ERB lookups fall back to the json/* views.
|
|
36
|
+
def base_format
|
|
37
|
+
'json'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def beautify
|
|
41
|
+
data = JSON.parse("{#{buffer.chomp.chomp(',')}}")
|
|
42
|
+
puts JSON.pretty_generate(sarif_document(data))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def sarif_document(data)
|
|
48
|
+
rules = {}
|
|
49
|
+
results = []
|
|
50
|
+
|
|
51
|
+
collect_vulnerability_results(data, rules, results)
|
|
52
|
+
collect_interesting_finding_results(data, rules, results)
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
'$schema' => SARIF_SCHEMA,
|
|
56
|
+
'version' => SARIF_VERSION,
|
|
57
|
+
'runs' => [build_run(data, rules, results)]
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_run(data, rules, results)
|
|
62
|
+
{
|
|
63
|
+
'tool' => {
|
|
64
|
+
'driver' => {
|
|
65
|
+
'name' => 'WPScan',
|
|
66
|
+
'version' => WPScan::VERSION,
|
|
67
|
+
'informationUri' => INFO_URI,
|
|
68
|
+
'rules' => rules.values
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
'invocations' => [invocation(data)].compact,
|
|
72
|
+
'results' => results,
|
|
73
|
+
'properties' => run_properties(data)
|
|
74
|
+
}.compact
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def collect_vulnerability_results(data, rules, results)
|
|
78
|
+
components(data).each do |component|
|
|
79
|
+
Array(component[:vulnerabilities]).each do |vuln|
|
|
80
|
+
rule_id = rule_id_for(vuln)
|
|
81
|
+
rules[rule_id] ||= rule_for(vuln, rule_id)
|
|
82
|
+
results << vulnerability_result(vuln, rule_id, component)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def collect_interesting_finding_results(data, rules, results)
|
|
88
|
+
Array(data['interesting_findings']).each do |finding|
|
|
89
|
+
rule_id = "wpscan.interesting-finding.#{finding['type'] || 'unknown'}"
|
|
90
|
+
rules[rule_id] ||= interesting_finding_rule(rule_id, finding)
|
|
91
|
+
results << interesting_finding_result(finding, rule_id)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [ Array<Hash> ] one entry per WordPress component observed
|
|
96
|
+
def components(data)
|
|
97
|
+
list = []
|
|
98
|
+
list << core_component(data) if data['version']
|
|
99
|
+
list << theme_component(data['main_theme']) if data['main_theme']
|
|
100
|
+
(data['themes'] || {}).each_value { |theme| list << theme_component(theme) }
|
|
101
|
+
(data['plugins'] || {}).each { |slug, plugin| list << plugin_component(slug, plugin) }
|
|
102
|
+
list
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def core_component(data)
|
|
106
|
+
{
|
|
107
|
+
kind: 'core',
|
|
108
|
+
slug: nil,
|
|
109
|
+
version: data['version']['number'],
|
|
110
|
+
url: data['effective_url'] || data['target_url'],
|
|
111
|
+
vulnerabilities: data['version']['vulnerabilities']
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def plugin_component(slug, plugin)
|
|
116
|
+
{
|
|
117
|
+
kind: 'plugin',
|
|
118
|
+
slug: slug,
|
|
119
|
+
version: plugin.dig('version', 'number'),
|
|
120
|
+
url: plugin['location'],
|
|
121
|
+
vulnerabilities: plugin['vulnerabilities']
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def theme_component(theme)
|
|
126
|
+
{
|
|
127
|
+
kind: 'theme',
|
|
128
|
+
slug: theme['slug'],
|
|
129
|
+
version: theme.dig('version', 'number'),
|
|
130
|
+
url: theme['location'],
|
|
131
|
+
vulnerabilities: theme['vulnerabilities']
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def rule_id_for(vuln)
|
|
136
|
+
cve = Array(vuln.dig('references', 'cve')).first
|
|
137
|
+
return "CVE-#{cve}" if cve
|
|
138
|
+
|
|
139
|
+
wpvulndb = Array(vuln.dig('references', 'wpvulndb')).first
|
|
140
|
+
return "WPVULNDB-#{wpvulndb}" if wpvulndb
|
|
141
|
+
|
|
142
|
+
# Stable fallback so we don't emit duplicate rule entries for the same title.
|
|
143
|
+
"wpscan.vuln.#{Digest::SHA1.hexdigest(vuln['title'].to_s)[0, 12]}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def rule_for(vuln, rule_id)
|
|
147
|
+
title = vuln['title'].to_s
|
|
148
|
+
{
|
|
149
|
+
'id' => rule_id,
|
|
150
|
+
'name' => rule_id.gsub(/[^A-Za-z0-9_]/, '_'),
|
|
151
|
+
'shortDescription' => { 'text' => title },
|
|
152
|
+
'fullDescription' => { 'text' => title },
|
|
153
|
+
'helpUri' => help_uri_for(vuln),
|
|
154
|
+
'defaultConfiguration' => { 'level' => level_for(vuln) },
|
|
155
|
+
'messageStrings' => {
|
|
156
|
+
# {0} = component label (e.g. "Plugin 'foo' 1.2.3"), {1} = fix status.
|
|
157
|
+
# Title is baked in here so the rule owns its phrasing and the result
|
|
158
|
+
# message stays small (SARIF2002). Dynamic args are single-quoted per
|
|
159
|
+
# SARIF2015 and the message terminates with a period per SARIF2001.
|
|
160
|
+
'default' => { 'text' => "'{0}': #{title} ('{1}')." }
|
|
161
|
+
},
|
|
162
|
+
'properties' => rule_properties(vuln)
|
|
163
|
+
}.compact
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def vulnerability_result(vuln, rule_id, component)
|
|
167
|
+
{
|
|
168
|
+
'ruleId' => rule_id,
|
|
169
|
+
'level' => level_for(vuln),
|
|
170
|
+
'message' => {
|
|
171
|
+
'id' => 'default',
|
|
172
|
+
'arguments' => [component_label(component), fix_status(vuln)]
|
|
173
|
+
},
|
|
174
|
+
'locations' => [location_for(component[:url], logical_location(component))]
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def interesting_finding_rule(rule_id, finding)
|
|
179
|
+
{
|
|
180
|
+
'id' => rule_id,
|
|
181
|
+
'name' => rule_id.gsub(/[^A-Za-z0-9_]/, '_'),
|
|
182
|
+
'shortDescription' => { 'text' => "Interesting finding: #{finding['type']}" },
|
|
183
|
+
# Non-vulnerability findings (enumeration results, headers, robots.txt entries,
|
|
184
|
+
# etc.) are emitted at `note` level so they don't drown out real vulnerabilities
|
|
185
|
+
# in Code Scanning's UI — see GitHub's SARIF support guidance.
|
|
186
|
+
'defaultConfiguration' => { 'level' => 'note' },
|
|
187
|
+
'messageStrings' => {
|
|
188
|
+
# {0} = finding header (to_s), {1} = interesting entries joined,
|
|
189
|
+
# {2} = found-by strategy, {3} = confidence percentage.
|
|
190
|
+
'default' => {
|
|
191
|
+
'text' => "'{0}' — entries: '{1}'; found by: '{2}'; confidence: '{3}'."
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
'helpUri' => INFO_URI
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def interesting_finding_result(finding, rule_id)
|
|
199
|
+
location = location_for(finding['url'], nil)
|
|
200
|
+
entries = Array(finding['interesting_entries']).join(' | ')
|
|
201
|
+
{
|
|
202
|
+
'ruleId' => rule_id,
|
|
203
|
+
'level' => 'note',
|
|
204
|
+
'message' => {
|
|
205
|
+
'id' => 'default',
|
|
206
|
+
'arguments' => [
|
|
207
|
+
finding['to_s'].to_s,
|
|
208
|
+
entries,
|
|
209
|
+
finding['found_by'].to_s,
|
|
210
|
+
finding['confidence'].to_s
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
'locations' => location ? [location] : []
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def location_for(url, logical)
|
|
218
|
+
physical = physical_location(url)
|
|
219
|
+
return nil if physical.nil? && logical.nil?
|
|
220
|
+
|
|
221
|
+
loc = {}
|
|
222
|
+
loc['physicalLocation'] = physical if physical
|
|
223
|
+
loc['logicalLocations'] = [logical] if logical
|
|
224
|
+
loc
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def physical_location(url)
|
|
228
|
+
return nil if url.nil? || url.to_s.empty?
|
|
229
|
+
|
|
230
|
+
{ 'artifactLocation' => { 'uri' => url.to_s } }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def logical_location(component)
|
|
234
|
+
fqn = case component[:kind]
|
|
235
|
+
when 'core' then "wordpress.core@#{component[:version] || 'unknown'}"
|
|
236
|
+
when 'theme' then "wordpress.theme.#{component[:slug]}@#{component[:version] || 'unknown'}"
|
|
237
|
+
when 'plugin' then "wordpress.plugin.#{component[:slug]}@#{component[:version] || 'unknown'}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
{
|
|
241
|
+
'name' => component[:slug] || component[:kind],
|
|
242
|
+
'fullyQualifiedName' => fqn,
|
|
243
|
+
'kind' => 'module'
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Map CVSS v3 base score to a SARIF level. Vulnerabilities without a
|
|
248
|
+
# score default to `error` since WPScan only flags entries the WPScan
|
|
249
|
+
# vulnerability database considers exploitable.
|
|
250
|
+
def level_for(vuln)
|
|
251
|
+
score = vuln.dig('cvss', 'score').to_f
|
|
252
|
+
return 'error' if score >= 7.0
|
|
253
|
+
return 'warning' if score >= 4.0
|
|
254
|
+
return 'note' if score.positive?
|
|
255
|
+
|
|
256
|
+
'error'
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def component_label(component)
|
|
260
|
+
case component[:kind]
|
|
261
|
+
when 'core' then "WordPress core #{component[:version]}"
|
|
262
|
+
when 'theme' then "Theme '#{component[:slug]}' #{component[:version]}"
|
|
263
|
+
when 'plugin' then "Plugin '#{component[:slug]}' #{component[:version]}"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def fix_status(vuln)
|
|
268
|
+
vuln['fixed_in'] ? "fixed in #{vuln['fixed_in']}" : 'no fix available'
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def help_uri_for(vuln)
|
|
272
|
+
wpvulndb = Array(vuln.dig('references', 'wpvulndb')).first
|
|
273
|
+
return "https://wpscan.com/vulnerability/#{wpvulndb}" if wpvulndb
|
|
274
|
+
|
|
275
|
+
Array(vuln.dig('references', 'url')).first
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def rule_properties(vuln)
|
|
279
|
+
props = {}
|
|
280
|
+
props['cvss'] = vuln['cvss'] if vuln['cvss']
|
|
281
|
+
props['references'] = vuln['references'] if vuln['references'] && !vuln['references'].empty?
|
|
282
|
+
props['fixed_in'] = vuln['fixed_in'] if vuln['fixed_in']
|
|
283
|
+
props['poc'] = vuln['poc'] if vuln['poc']
|
|
284
|
+
props.empty? ? nil : props
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def invocation(data)
|
|
288
|
+
inv = { 'executionSuccessful' => true }
|
|
289
|
+
inv['commandLine'] = "wpscan #{data['command_line']}" if data['command_line']
|
|
290
|
+
inv['hostname'] = data['hostname'] if data['hostname']
|
|
291
|
+
inv['startTimeUtc'] = to_iso8601(data['start_time']) if data['start_time']
|
|
292
|
+
inv['endTimeUtc'] = to_iso8601(data['stop_time']) if data['stop_time']
|
|
293
|
+
inv
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def run_properties(data)
|
|
297
|
+
props = {}
|
|
298
|
+
props['targetUrl'] = data['target_url'] if data['target_url']
|
|
299
|
+
props['effectiveUrl'] = data['effective_url'] if data['effective_url']
|
|
300
|
+
props['targetIp'] = data['target_ip'] if data['target_ip']
|
|
301
|
+
props.empty? ? nil : props
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def to_iso8601(epoch)
|
|
305
|
+
return nil if epoch.nil? || epoch.to_i.zero?
|
|
306
|
+
|
|
307
|
+
Time.at(epoch.to_i).utc.strftime('%Y-%m-%dT%H:%M:%S.000Z')
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Model
|
|
5
|
+
# BackupFolder
|
|
6
|
+
class BackupFolder < InterestingFinding
|
|
7
|
+
MAX_ENTRIES_DISPLAY = 10
|
|
8
|
+
|
|
9
|
+
# @return [ String ]
|
|
10
|
+
def to_s
|
|
11
|
+
msg = "Backup folder found: #{url}"
|
|
12
|
+
if interesting_entries&.any?
|
|
13
|
+
total = @interesting_entries.size
|
|
14
|
+
msg += " (#{total} #{total == 1 ? 'entry' : 'entries'})"
|
|
15
|
+
end
|
|
16
|
+
msg
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [ Symbol ]
|
|
20
|
+
def severity
|
|
21
|
+
return :high if interesting_entries&.any?
|
|
22
|
+
|
|
23
|
+
:medium
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Limit displayed entries to avoid overwhelming output
|
|
27
|
+
# @return [ Array<String> ]
|
|
28
|
+
def interesting_entries
|
|
29
|
+
return [] unless @interesting_entries
|
|
30
|
+
|
|
31
|
+
entries = @interesting_entries.first(MAX_ENTRIES_DISPLAY)
|
|
32
|
+
if @interesting_entries.size > MAX_ENTRIES_DISPLAY
|
|
33
|
+
entries << "... and #{@interesting_entries.size - MAX_ENTRIES_DISPLAY} more"
|
|
34
|
+
end
|
|
35
|
+
entries
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Model
|
|
5
|
+
# Fantastico is a commercial script library that automates the installation of web applications to a website.
|
|
6
|
+
# Fantastico scripts are executed from the administration area of a website control panel such as cPanel.
|
|
7
|
+
# It creates a file named fantastico_fileslist.txt that is publicly available and contains a list of all the
|
|
8
|
+
# files from the current directory. The contents of this file may expose sensitive information to an attacker.
|
|
9
|
+
class FantasticoFileslist < InterestingFinding
|
|
10
|
+
# @return [ String ]
|
|
11
|
+
def to_s
|
|
12
|
+
@to_s ||= "Fantastico list found: #{url}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [ Array<String> ] The interesting files/dirs detected
|
|
16
|
+
def interesting_entries
|
|
17
|
+
results = []
|
|
18
|
+
|
|
19
|
+
entries.each do |entry|
|
|
20
|
+
next unless /(?:admin|\.log|\.sql|\.db)/i.match?(entry)
|
|
21
|
+
|
|
22
|
+
results << entry
|
|
23
|
+
end
|
|
24
|
+
results
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def references
|
|
28
|
+
@references ||= {
|
|
29
|
+
url: ['https://web.archive.org/web/20140518040021/http://www.acunetix.com/vulnerabilities/fantastico-fileslist/']
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Model
|
|
5
|
+
# Interesting Headers
|
|
6
|
+
class Headers < InterestingFinding
|
|
7
|
+
# @return [ Hash ] The headers
|
|
8
|
+
def entries
|
|
9
|
+
res = WPScan::Browser.get(url)
|
|
10
|
+
return [] unless res&.headers
|
|
11
|
+
|
|
12
|
+
res.headers
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [ Array<String> ] The interesting headers detected
|
|
16
|
+
def interesting_entries
|
|
17
|
+
results = []
|
|
18
|
+
|
|
19
|
+
entries.each do |header, value|
|
|
20
|
+
next if known_headers.include?(header.downcase)
|
|
21
|
+
|
|
22
|
+
results << "#{header}: #{Array(value).join(', ')}"
|
|
23
|
+
end
|
|
24
|
+
results
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [ Array<String> ] Downcased known headers
|
|
28
|
+
def known_headers
|
|
29
|
+
%w[
|
|
30
|
+
age accept-ranges cache-control content-encoding content-length content-type connection date
|
|
31
|
+
etag expires keep-alive location last-modified link pragma set-cookie strict-transport-security
|
|
32
|
+
transfer-encoding vary x-cache x-content-security-policy x-content-type-options
|
|
33
|
+
x-frame-options x-language x-permitted-cross-domain-policies x-pingback x-varnish
|
|
34
|
+
x-webkit-csp x-xss-protection
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [ String ]
|
|
39
|
+
def to_s
|
|
40
|
+
@to_s ||= 'Headers'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -2,9 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
module WPScan
|
|
4
4
|
module Model
|
|
5
|
-
#
|
|
6
|
-
class InterestingFinding
|
|
5
|
+
# Interesting Finding base class.
|
|
6
|
+
class InterestingFinding
|
|
7
|
+
include Finders::Finding
|
|
7
8
|
include References
|
|
9
|
+
|
|
10
|
+
attr_reader :url
|
|
11
|
+
attr_writer :to_s
|
|
12
|
+
|
|
13
|
+
# @param [ String ] url
|
|
14
|
+
# @param [ Hash ] opts
|
|
15
|
+
# :to_s (override the to_s method)
|
|
16
|
+
# See Finders::Finding for other available options
|
|
17
|
+
def initialize(url, opts = {})
|
|
18
|
+
@url = url
|
|
19
|
+
@to_s = opts[:to_s]
|
|
20
|
+
|
|
21
|
+
parse_finding_options(opts)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [ Array<String> ]
|
|
25
|
+
def entries
|
|
26
|
+
res = WPScan::Browser.get(url)
|
|
27
|
+
|
|
28
|
+
return [] unless res && res.headers['Content-Type'] =~ %r{\Atext/plain;}i
|
|
29
|
+
|
|
30
|
+
res.body.split("\n").reject { |s| s.strip.empty? }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [ String ]
|
|
34
|
+
def to_s
|
|
35
|
+
@to_s || url
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [ String ]
|
|
39
|
+
def type
|
|
40
|
+
@type ||= self.class.to_s.demodulize.underscore
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [ Boolean ]
|
|
44
|
+
def ==(other)
|
|
45
|
+
self.class == other.class && to_s == other.to_s
|
|
46
|
+
end
|
|
8
47
|
end
|
|
9
48
|
|
|
10
49
|
class BackupDB < InterestingFinding
|
data/app/models/plugin.rb
CHANGED
|
@@ -6,7 +6,7 @@ module WPScan
|
|
|
6
6
|
class Plugin < WpItem
|
|
7
7
|
# See WpItem
|
|
8
8
|
def initialize(slug, blog, opts = {})
|
|
9
|
-
super
|
|
9
|
+
super
|
|
10
10
|
|
|
11
11
|
# To be used by #head_and_get
|
|
12
12
|
# If custom wp-content, it will be replaced by blog#url
|
|
@@ -36,9 +36,15 @@ module WPScan
|
|
|
36
36
|
@version
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# @return [ String ]
|
|
40
|
+
def wordpress_org_api_url
|
|
41
|
+
encoded_slug = Addressable::URI.encode_component(slug, Addressable::URI::CharacterClasses::UNRESERVED)
|
|
42
|
+
"https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=#{encoded_slug}"
|
|
43
|
+
end
|
|
44
|
+
|
|
39
45
|
# @return [ Array<String> ]
|
|
40
46
|
def potential_readme_filenames
|
|
41
|
-
@potential_readme_filenames ||= Array(
|
|
47
|
+
@potential_readme_filenames ||= Array(DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super)
|
|
42
48
|
end
|
|
43
49
|
end
|
|
44
50
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Model
|
|
5
|
+
# Robots.txt
|
|
6
|
+
class RobotsTxt < InterestingFinding
|
|
7
|
+
# @return [ String ]
|
|
8
|
+
def to_s
|
|
9
|
+
@to_s ||= "robots.txt found: #{url}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @todo Better detection, currently everything not empty or / is returned
|
|
13
|
+
#
|
|
14
|
+
# @return [ Array<String> ] The interesting Allow/Disallow rules detected
|
|
15
|
+
def interesting_entries
|
|
16
|
+
results = []
|
|
17
|
+
|
|
18
|
+
entries.each do |entry|
|
|
19
|
+
next unless entry =~ /\A(?:dis)?allow:\s*(.+)\z/i
|
|
20
|
+
|
|
21
|
+
match = Regexp.last_match(1)
|
|
22
|
+
next if match == '/'
|
|
23
|
+
|
|
24
|
+
results << match
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
results.uniq
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Model
|
|
5
|
+
# SearchReplaceDB2
|
|
6
|
+
class SearchReplaceDB2 < InterestingFinding
|
|
7
|
+
# @return [ String ]
|
|
8
|
+
def to_s
|
|
9
|
+
@to_s ||= "Search Replace DB script found: #{url}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def references
|
|
13
|
+
@references ||= { url: ['https://interconnectit.com/products/search-and-replace-for-wordpress-databases/'] }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/app/models/theme.rb
CHANGED
|
@@ -9,7 +9,7 @@ module WPScan
|
|
|
9
9
|
|
|
10
10
|
# See WpItem
|
|
11
11
|
def initialize(slug, blog, opts = {})
|
|
12
|
-
super
|
|
12
|
+
super
|
|
13
13
|
|
|
14
14
|
# To be used by #head_and_get
|
|
15
15
|
# If custom wp-content, it will be replaced by blog#url
|
|
@@ -42,6 +42,13 @@ module WPScan
|
|
|
42
42
|
@version
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# @return [ String ]
|
|
46
|
+
def wordpress_org_api_url
|
|
47
|
+
encoded_slug = Addressable::URI.encode_component(slug, Addressable::URI::CharacterClasses::UNRESERVED)
|
|
48
|
+
'https://api.wordpress.org/themes/info/1.2/?action=theme_information' \
|
|
49
|
+
"&request[slug]=#{encoded_slug}&request[fields][active_installs]=1&request[fields][last_updated]=1"
|
|
50
|
+
end
|
|
51
|
+
|
|
45
52
|
# @return [ Theme ]
|
|
46
53
|
def parent_theme
|
|
47
54
|
return unless template
|
|
@@ -107,7 +114,7 @@ module WPScan
|
|
|
107
114
|
end
|
|
108
115
|
|
|
109
116
|
def ==(other)
|
|
110
|
-
super
|
|
117
|
+
super && style_url == other.style_url
|
|
111
118
|
end
|
|
112
119
|
end
|
|
113
120
|
end
|
data/app/models/timthumb.rb
CHANGED
|
@@ -12,7 +12,7 @@ module WPScan
|
|
|
12
12
|
# @param [ Hash ] opts
|
|
13
13
|
# @option opts [ Symbol ] :mode The mode to use to detect the version
|
|
14
14
|
def initialize(url, opts = {})
|
|
15
|
-
super
|
|
15
|
+
super
|
|
16
16
|
|
|
17
17
|
@version_detection_opts = opts[:version_detection] || {}
|
|
18
18
|
end
|
|
@@ -63,7 +63,7 @@ module WPScan
|
|
|
63
63
|
def webshot_enabled?
|
|
64
64
|
res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
!res.body.include?('WEBSHOT_ENABLED == true')
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
|
data/app/models/user.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Model
|
|
5
|
+
# User
|
|
6
|
+
class User
|
|
7
|
+
include Finders::Finding
|
|
8
|
+
|
|
9
|
+
attr_accessor :password
|
|
10
|
+
attr_reader :id, :username
|
|
11
|
+
|
|
12
|
+
# @param [ String ] username
|
|
13
|
+
# @param [ Hash ] opts
|
|
14
|
+
# @option opts [ Integer ] :id
|
|
15
|
+
# @option opts [ String ] :password
|
|
16
|
+
def initialize(username, opts = {})
|
|
17
|
+
@username = username
|
|
18
|
+
@password = opts[:password]
|
|
19
|
+
@id = opts[:id]
|
|
20
|
+
|
|
21
|
+
parse_finding_options(opts)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ==(other)
|
|
25
|
+
return false unless self.class == other.class
|
|
26
|
+
|
|
27
|
+
username == other.username && password == other.password
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
username
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
module Model
|
|
5
|
+
# Version
|
|
6
|
+
class Version
|
|
7
|
+
include Finders::Finding
|
|
8
|
+
|
|
9
|
+
attr_reader :number
|
|
10
|
+
|
|
11
|
+
def initialize(number, opts = {})
|
|
12
|
+
@number = number.to_s
|
|
13
|
+
@number = "0#{number}" if @number[0, 1] == '.'
|
|
14
|
+
|
|
15
|
+
parse_finding_options(opts)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param [ Version, String ] other
|
|
19
|
+
# rubocop:disable Style/NumericPredicate
|
|
20
|
+
def ==(other)
|
|
21
|
+
(self <=> other) == 0
|
|
22
|
+
end
|
|
23
|
+
# rubocop:enable all
|
|
24
|
+
|
|
25
|
+
# @param [ Version, String ] other
|
|
26
|
+
def <(other)
|
|
27
|
+
(self <=> other) == -1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param [ Version, String ] other
|
|
31
|
+
def >(other)
|
|
32
|
+
(self <=> other) == 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param [ Version, String ] other
|
|
36
|
+
def <=>(other)
|
|
37
|
+
other = self.class.new(other) unless other.is_a?(self.class) # handle potential '.1' version
|
|
38
|
+
|
|
39
|
+
Gem::Version.new(number) <=> Gem::Version.new(other.number)
|
|
40
|
+
rescue ArgumentError
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_s
|
|
45
|
+
number
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|