wpscan 4.0.1 → 4.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6e5b02dd5de5e0624c1dca1b8bb3bad6f16ff008fe054957e80c3ce33807a70
4
- data.tar.gz: fb7cec3911f60cabbf70ac8e34672e4a41fe68a64bd76e9d7b55f0f5e901d012
3
+ metadata.gz: c6215ce3d361d0754721dab264b279b43c0064cb2621a14720db0e15167bbc52
4
+ data.tar.gz: 6c092df8d32bb9f5e0a750c2801fb7cd086c6f258f404023581dc61c194fb962
5
5
  SHA512:
6
- metadata.gz: 724a6be1ce5d754a9891781e7101bf5b504c056107d1a7dc4cb0bb107a03044ccfff73dd648f32e14bcc0d8299b8b7872e2cbdd8f813d928a2abd64402ed28f0
7
- data.tar.gz: 9c31899c0110f312f496a934f585e661103a364cf535da47a49d9002594eb047b942715a299cf275fdc85c1143a725cb5ee9d3f3d86dcbe29a3e53f7a400c432
6
+ metadata.gz: b74ca0ee959b570d23d23d51936f121e31a16ef73ec896c5b1f6e97f503d6000c129cb3655fac0b6bca8072f7047c2bfad46711b77fb16f4f4cbdd9069a10311
7
+ data.tar.gz: 3067ba3dc843c74014b1bbda40ca2be6549fc17924aa64456128a667f91d20957ef86c1d79b430767fc2cb9a4a4b1b9fbb3bfd1d3f92c5edf94bb6c993ae8b23
data/README.md CHANGED
@@ -144,6 +144,8 @@ The WPScan CLI tool uses the [WordPress Vulnerability Database API](https://wpsc
144
144
 
145
145
  Up to **25** API requests per day are given free of charge, that should be suitable to scan most WordPress websites at least once per day. When the daily 25 API requests are exhausted, WPScan will continue to work as normal but without any vulnerability data.
146
146
 
147
+ Enterprise customers can alternatively scan against a local database dump with no request limits — see [Enterprise Local Database (opt-in)](#enterprise-local-database-opt-in).
148
+
147
149
  ### How many API requests do you need?
148
150
 
149
151
  - Our WordPress scanner makes one API request for the WordPress version, one request per installed plugin, and one request per the installed theme.
@@ -205,6 +207,28 @@ cli_options:
205
207
 
206
208
  The API Token will be automatically loaded from the ENV variable `WPSCAN_API_TOKEN` if present. If the `--api-token` CLI option is also provided, the value from the CLI will be used.
207
209
 
210
+ ## Enterprise Local Database (opt-in)
211
+
212
+ Enterprise customers can scan using a **local vulnerability database dump** instead of the online API, if they prefer to do so for privacy. Supply the enterprise token via the `--enterprise-db-token TOKEN` option:
213
+
214
+ ```bash
215
+ wpscan --url https://example.com --enterprise-db-token YOUR_ENTERPRISE_DB_TOKEN
216
+ ```
217
+
218
+ When this option is used:
219
+
220
+ - During the normal database update, WPScan additionally downloads `plugins.json.gz`, `themes.json.gz` and `wordpresses.json.gz` from `enterprise-data.wpscan.org` (authenticated with the token). As with the rest of the database, these are only re-downloaded when they change.
221
+ - Plugin, theme and WordPress vulnerability data is then read from those local dumps; no requests are made to the WPScan API.
222
+
223
+ The token can also be provided via the `WPSCAN_ENTERPRISE_DB_TOKEN` environment variable, or the `enterprise_db_token` key in a configuration file (see [Load CLI options from file/s](#load-cli-options-from-files)):
224
+
225
+ ```yml
226
+ cli_options:
227
+ enterprise_db_token: 'YOUR_ENTERPRISE_DB_TOKEN'
228
+ ```
229
+
230
+ `--enterprise-db-token` and `--api-token` are mutually exclusive; supplying both (including via their respective environment variables) results in an error.
231
+
208
232
  ## API Service Status
209
233
 
210
234
  If you experience connection issues with the WPScan API, you can check the service status at https://status.wpscan.com/. When API connection errors occur, WPScan will include a link to the status page in the error message.
@@ -37,6 +37,12 @@ module WPScan
37
37
 
38
38
  maybe_output_banner_help_and_version
39
39
 
40
+ # Fail fast on conflicting API tokens, before the DB update (which would download the
41
+ # enterprise DB dumps) and before any request is sent to the target. Done here rather
42
+ # than only in VulnApi#before_scan (which runs after this controller) so that the
43
+ # conflict also aborts DB-update-only runs (--update without --url).
44
+ VulnApi.validate_api_tokens!
45
+
40
46
  update_db if update_db_required?
41
47
  setup_cache
42
48
  check_target_availability
@@ -14,8 +14,10 @@ module WPScan
14
14
  # requirement for `vp`/`vt` is irrelevant in that case.
15
15
  return if ParsedCli.wp_auth
16
16
 
17
- # Check if vulnerable plugin/theme enumeration is requested without an API token
18
- return unless (enum[:vulnerable_plugins] || enum[:vulnerable_themes]) && DB::VulnApi.token.nil?
17
+ # Check if vulnerable plugin/theme enumeration is requested without an API token.
18
+ # The local enterprise DB dump (--enterprise-db-token) provides the same data without a token.
19
+ return unless (enum[:vulnerable_plugins] || enum[:vulnerable_themes]) &&
20
+ DB::VulnApi.token.nil? && !DB::VulnApi.local_db
19
21
 
20
22
  raise Error::ApiTokenRequiredForVulnerableEnumeration
21
23
  end
@@ -4,7 +4,28 @@ module WPScan
4
4
  module Controller
5
5
  # Controller to handle the API token
6
6
  class VulnApi < WPScan::Controller::Base
7
- ENV_KEY = 'WPSCAN_API_TOKEN'
7
+ ENV_KEY = 'WPSCAN_API_TOKEN'
8
+ ENTERPRISE_ENV_KEY = 'WPSCAN_ENTERPRISE_DB_TOKEN'
9
+
10
+ # Class level so that Controller::Core (which runs first) can validate the tokens right after
11
+ # the CLI options have been parsed, aborting before anything else (DB update, requests to the
12
+ # target) is done.
13
+ class << self
14
+ # @return [ String, nil ] The enterprise DB token (CLI or ENV). Must match DB::Updater's resolution.
15
+ def enterprise_db_token
16
+ ParsedCli.enterprise_db_token || ENV.fetch(ENTERPRISE_ENV_KEY, nil)
17
+ end
18
+
19
+ # @return [ String, nil ] The API token (CLI or ENV var)
20
+ def api_token
21
+ ParsedCli.api_token || ENV.fetch(ENV_KEY, nil)
22
+ end
23
+
24
+ # @raise [ Error::ConflictingApiTokens ] When both the API and enterprise DB tokens are supplied
25
+ def validate_api_tokens!
26
+ raise Error::ConflictingApiTokens if enterprise_db_token && api_token
27
+ end
28
+ end
8
29
 
9
30
  def cli_options
10
31
  [
@@ -12,6 +33,14 @@ module WPScan
12
33
  ['--api-token TOKEN',
13
34
  'The WPScan API Token to display vulnerability data, available at https://wpscan.com/profile']
14
35
  ),
36
+ OptString.new(
37
+ ['--enterprise-db-token TOKEN',
38
+ 'Use a local enterprise vulnerability database dump instead of the WPScan API. The ' \
39
+ 'plugins/themes/wordpresses dumps are downloaded from enterprise-data.wpscan.org using ' \
40
+ 'this token during the database update, then read locally (no per-finding API calls). Mutually ' \
41
+ "exclusive with --api-token. Can also be set via the #{ENTERPRISE_ENV_KEY} environment variable."],
42
+ { advanced: true }
43
+ ),
15
44
  OptBoolean.new(
16
45
  ['--proxy-target-only',
17
46
  'When used with --proxy, the proxy is only applied to requests made to the target, ' \
@@ -22,9 +51,15 @@ module WPScan
22
51
  end
23
52
 
24
53
  def before_scan
25
- return unless ParsedCli.api_token || ENV.key?(ENV_KEY)
54
+ # Already done by Core#before_scan (before the DB update, to fail as early as possible),
55
+ # kept as a safety net in case this controller is used in a chain without Core.
56
+ self.class.validate_api_tokens!
57
+
58
+ return setup_enterprise_db if enterprise_db_token
26
59
 
27
- DB::VulnApi.token = ParsedCli.api_token || ENV.fetch(ENV_KEY, nil)
60
+ return unless api_token
61
+
62
+ DB::VulnApi.token = api_token
28
63
 
29
64
  api_status = DB::VulnApi.status
30
65
 
@@ -36,6 +71,28 @@ module WPScan
36
71
  def after_scan
37
72
  output('status', status: DB::VulnApi.status, api_requests: WPScan.api_requests)
38
73
  end
74
+
75
+ private
76
+
77
+ # @return [ String, nil ] The enterprise DB token (CLI or ENV)
78
+ def enterprise_db_token
79
+ self.class.enterprise_db_token
80
+ end
81
+
82
+ # @return [ String, nil ] The API token (CLI or ENV var)
83
+ def api_token
84
+ self.class.api_token
85
+ end
86
+
87
+ # Switches DB::VulnApi to local-dump mode and ensures the dumps are present. Core's DB
88
+ # update normally downloads them before this controller runs (VulnApi is chained after Core).
89
+ def setup_enterprise_db
90
+ DB::VulnApi.local_db = true
91
+
92
+ missing = DB::VulnApi::ENTERPRISE_DB_FILES.values.reject { |file| File.exist?(DB_DIR.join(file)) }
93
+
94
+ raise Error::MissingEnterpriseDatabaseFile, missing unless missing.empty?
95
+ end
39
96
  end
40
97
  end
41
98
  end
@@ -39,7 +39,7 @@ module WPScan
39
39
 
40
40
  @vulnerabilities = []
41
41
 
42
- Array(db_data['vulnerabilities']).each do |json_vuln|
42
+ Vulnerability.sort_json(db_data['vulnerabilities']).each do |json_vuln|
43
43
  vulnerability = Vulnerability.load_from_json(json_vuln)
44
44
  vulnerability.detected_version = version || nil
45
45
  @vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
@@ -57,7 +57,7 @@ module WPScan
57
57
 
58
58
  @vulnerabilities = []
59
59
 
60
- Array(db_data['vulnerabilities']).each do |json_vuln|
60
+ Vulnerability.sort_json(db_data['vulnerabilities']).each do |json_vuln|
61
61
  vulnerability = Vulnerability.load_from_json(json_vuln)
62
62
  vulnerability.detected_version = self
63
63
  @vulnerabilities << vulnerability
@@ -4,7 +4,7 @@ require_relative 'config_files_loader_merger/base'
4
4
  require_relative 'config_files_loader_merger/json'
5
5
  require_relative 'config_files_loader_merger/yml'
6
6
 
7
- # :nocov:
7
+ # simplecov:disable
8
8
  # @param [ String ] path The path of the file to load
9
9
  def yaml_safe_load(path)
10
10
  if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1') # Ruby 2.6
@@ -13,7 +13,7 @@ def yaml_safe_load(path)
13
13
  YAML.safe_load_file(path, [Regexp]) || {}
14
14
  end
15
15
  end
16
- # :nocov:
16
+ # simplecov:enable
17
17
 
18
18
  module OptParseValidator
19
19
  # Options Files container
@@ -3,7 +3,7 @@
3
3
  module WPScan
4
4
  module DB
5
5
  # Class used to perform DB updates
6
- # :nocov:
6
+ # simplecov:disable
7
7
  class Updater
8
8
  # /!\ Might want to also update the Enumeration#cli_options when some filenames are changed here
9
9
  FILES = %w[
@@ -17,6 +17,13 @@ module WPScan
17
17
  wordpresses.json plugins.json themes.json
18
18
  ].freeze
19
19
 
20
+ # Enterprise vulnerability DB dumps, downloaded (in addition to FILES) only when an
21
+ # --enterprise-db-token is supplied, from ENTERPRISE_HOST using the X-DB-JSON-AUTH header.
22
+ ENTERPRISE_FILES = %w[wordpresses.json.gz plugins.json.gz themes.json.gz].freeze
23
+
24
+ ENTERPRISE_HOST = 'enterprise-data.wpscan.org'
25
+ DEFAULT_HOST = 'data.wpscan.org'
26
+
20
27
  attr_reader :repo_directory
21
28
 
22
29
  def initialize(repo_directory)
@@ -43,6 +50,24 @@ module WPScan
43
50
  end
44
51
  end
45
52
 
53
+ # @return [ String, nil ] The enterprise DB token. Resolved exactly like Controller::VulnApi
54
+ # does; the two must stay in sync or the dumps won't be downloaded.
55
+ def enterprise_db_token
56
+ ParsedCli.enterprise_db_token || ENV.fetch('WPSCAN_ENTERPRISE_DB_TOKEN', nil)
57
+ end
58
+
59
+ # @return [ Array<String> ] The files to keep in sync (plus the enterprise dumps when a token is set)
60
+ def files
61
+ return FILES + ENTERPRISE_FILES if enterprise_db_token
62
+
63
+ FILES
64
+ end
65
+
66
+ # @return [ Boolean ] Whether the file is an enterprise dump (fetched from ENTERPRISE_HOST with auth)
67
+ def enterprise_file?(filename)
68
+ !enterprise_db_token.nil? && ENTERPRISE_FILES.include?(filename)
69
+ end
70
+
46
71
  # @return [ Time, nil ]
47
72
  def last_update
48
73
  Time.parse(File.read(last_update_file))
@@ -64,7 +89,7 @@ module WPScan
64
89
 
65
90
  # @return [ Boolean ]
66
91
  def missing_files?
67
- FILES.each do |file|
92
+ files.each do |file|
68
93
  return true unless File.exist?(repo_directory.join(file))
69
94
  end
70
95
  false
@@ -72,35 +97,44 @@ module WPScan
72
97
 
73
98
  # @return [ Hash ] The params for Typhoeus::Request
74
99
  # @note Those params can't be overriden by CLI options
75
- def request_params
76
- @request_params ||= begin
77
- params = Browser.instance.default_request_params.merge(
78
- timeout: 600,
79
- connecttimeout: 300,
80
- accept_encoding: 'gzip, deflate',
81
- cache_ttl: 0,
82
- headers: { 'User-Agent' => Browser.instance.default_user_agent }
83
- )
84
-
85
- if ParsedCli.proxy_target_only
86
- params.delete(:proxy)
87
- params.delete(:proxyuserpwd)
88
- end
89
-
90
- params
100
+ def request_params(filename = nil)
101
+ params = Browser.instance.default_request_params.merge(
102
+ timeout: 600,
103
+ connecttimeout: 300,
104
+ accept_encoding: 'gzip, deflate',
105
+ cache_ttl: 0,
106
+ headers: { 'User-Agent' => Browser.instance.default_user_agent }
107
+ )
108
+
109
+ if enterprise_file?(filename)
110
+ params[:headers]['X-DB-JSON-AUTH'] = enterprise_db_token
111
+ # The dumps are pre-gzipped static objects. If we advertise Accept-Encoding: gzip and the
112
+ # host responds with Content-Encoding: gzip, libcurl transparently decompresses at the
113
+ # transfer layer, so res.body would be plain JSON written into a *.json.gz file, breaking
114
+ # both Zlib::GzipReader and the SHA512 checksum (computed over the raw .gz).
115
+ params.delete(:accept_encoding)
116
+ end
117
+
118
+ if ParsedCli.proxy_target_only
119
+ params.delete(:proxy)
120
+ params.delete(:proxyuserpwd)
91
121
  end
122
+
123
+ params
92
124
  end
93
125
 
94
126
  # @return [ String ] The raw file URL associated with the given filename
95
127
  def remote_file_url(filename)
96
- "https://data.wpscan.org/#{filename}"
128
+ host = enterprise_file?(filename) ? ENTERPRISE_HOST : DEFAULT_HOST
129
+
130
+ "https://#{host}/#{filename}"
97
131
  end
98
132
 
99
133
  # @return [ String ] The checksum of the associated remote filename
100
134
  def remote_file_checksum(filename)
101
135
  url = "#{remote_file_url(filename)}.sha512"
102
136
 
103
- res = Typhoeus.get(url, request_params)
137
+ res = Typhoeus.get(url, request_params(filename))
104
138
  raise Error::Download, res if res.timed_out? || res.code != 200
105
139
 
106
140
  res.body.chomp
@@ -142,7 +176,7 @@ module WPScan
142
176
  file_path = local_file_path(filename)
143
177
  file_url = remote_file_url(filename)
144
178
 
145
- res = Typhoeus.get(file_url, request_params)
179
+ res = Typhoeus.get(file_url, request_params(filename))
146
180
  raise Error::Download, res if res.timed_out? || res.code != 200
147
181
 
148
182
  File.binwrite(file_path, res.body)
@@ -154,7 +188,7 @@ module WPScan
154
188
  def update
155
189
  updated = []
156
190
 
157
- FILES.each do |filename|
191
+ files.each do |filename|
158
192
  db_checksum = remote_file_checksum(filename)
159
193
 
160
194
  # Checking if the file needs to be updated
@@ -179,5 +213,5 @@ module WPScan
179
213
  end
180
214
  end
181
215
  end
182
- # :nocov:
216
+ # simplecov:enable
183
217
  end
@@ -1,13 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'zlib'
4
+
3
5
  module WPScan
4
6
  module DB
5
7
  # WPVulnDB API
6
8
  class VulnApi
7
9
  NON_ERROR_CODES = [200, 403].freeze
8
10
 
11
+ # Local (enterprise) vulnerability DB dumps, read instead of the API when local_db is set.
12
+ # Downloaded to DB_DIR by DB::Updater when --enterprise-db-token is supplied.
13
+ ENTERPRISE_DB_FILES = {
14
+ plugins: 'plugins.json.gz',
15
+ themes: 'themes.json.gz',
16
+ wordpresses: 'wordpresses.json.gz'
17
+ }.freeze
18
+
9
19
  class << self
10
- attr_accessor :token
20
+ # local_db switches vuln lookups from the API to the local dumps above
21
+ attr_accessor :token, :local_db
11
22
  end
12
23
 
13
24
  # @return [ Addressable::URI ]
@@ -15,6 +26,28 @@ module WPScan
15
26
  @uri ||= Addressable::URI.parse('https://wpscan.com/api/v4/')
16
27
  end
17
28
 
29
+ # @param [ String ] filename The gzipped JSON dump located in DB_DIR
30
+ #
31
+ # @return [ Hash ] The parsed content of the dump
32
+ def self.load_db(filename)
33
+ JSON.parse(Zlib::GzipReader.open(DB_DIR.join(filename).to_s, &:read))
34
+ end
35
+
36
+ # @return [ Hash ]
37
+ def self.plugins_db
38
+ @plugins_db ||= load_db(ENTERPRISE_DB_FILES[:plugins])
39
+ end
40
+
41
+ # @return [ Hash ]
42
+ def self.themes_db
43
+ @themes_db ||= load_db(ENTERPRISE_DB_FILES[:themes])
44
+ end
45
+
46
+ # @return [ Hash ]
47
+ def self.wordpresses_db
48
+ @wordpresses_db ||= load_db(ENTERPRISE_DB_FILES[:wordpresses])
49
+ end
50
+
18
51
  # @param [ String ] path
19
52
  # @param [ Hash ] params
20
53
  #
@@ -52,21 +85,30 @@ module WPScan
52
85
 
53
86
  # @return [ Hash ]
54
87
  def self.plugin_data(slug)
88
+ return plugins_db&.dig(slug) || {} if local_db
89
+
55
90
  get("plugins/#{slug}")&.dig(slug) || {}
56
91
  end
57
92
 
58
93
  # @return [ Hash ]
59
94
  def self.theme_data(slug)
95
+ return themes_db&.dig(slug) || {} if local_db
96
+
60
97
  get("themes/#{slug}")&.dig(slug) || {}
61
98
  end
62
99
 
63
100
  # @return [ Hash ]
64
101
  def self.wordpress_data(version_number)
102
+ # The dump is keyed by the dotted version; only the API URL strips the dots.
103
+ return wordpresses_db&.dig(version_number) || {} if local_db
104
+
65
105
  get("wordpresses/#{version_number.tr('.', '')}")&.dig(version_number) || {}
66
106
  end
67
107
 
68
108
  # @return [ Hash ]
69
109
  def self.status
110
+ return { 'plan' => 'enterprise', 'requests_remaining' => 'Unlimited', 'success' => true } if local_db
111
+
70
112
  json = get('status', params: { version: WPScan::VERSION }, cache_ttl: 0)
71
113
 
72
114
  json['requests_remaining'] = 'Unlimited' if json['requests_remaining'] == -1
@@ -61,20 +61,20 @@ module WPScan
61
61
 
62
62
  # HTTP Authentication Required Error
63
63
  class HTTPAuthRequired < Standard
64
- # :nocov:
64
+ # simplecov:disable
65
65
  def to_s
66
66
  'HTTP authentication required (or was invalid), please provide it with --http-auth'
67
67
  end
68
- # :nocov:
68
+ # simplecov:enable
69
69
  end
70
70
 
71
71
  # Proxy Authentication Required Error
72
72
  class ProxyAuthRequired < Standard
73
- # :nocov:
73
+ # simplecov:disable
74
74
  def to_s
75
75
  'Proxy authentication required (or was invalid), please provide it with --proxy-auth'
76
76
  end
77
- # :nocov:
77
+ # simplecov:enable
78
78
  end
79
79
 
80
80
  # Access Forbidden Error
@@ -4,11 +4,11 @@ module WPScan
4
4
  module Error
5
5
  # Used instead of the Timeout::Error
6
6
  class MaxScanDurationReached < Standard
7
- # :nocov:
7
+ # simplecov:disable
8
8
  def to_s
9
9
  'Max Scan Duration Reached'
10
10
  end
11
- # :nocov:
11
+ # simplecov:enable
12
12
  end
13
13
  end
14
14
  end
@@ -40,5 +40,29 @@ module WPScan
40
40
  'Please check https://status.wpscan.com/ for service status.'
41
41
  end
42
42
  end
43
+
44
+ # Error raised when both --api-token and --enterprise-db-token are supplied
45
+ class ConflictingApiTokens < Standard
46
+ def to_s
47
+ '--api-token and --enterprise-db-token are mutually exclusive, please provide only one ' \
48
+ '(this also applies to the WPSCAN_API_TOKEN and WPSCAN_ENTERPRISE_DB_TOKEN environment variables).'
49
+ end
50
+ end
51
+
52
+ # Error raised when --enterprise-db-token is set but a local DB dump is missing
53
+ class MissingEnterpriseDatabaseFile < Standard
54
+ attr_reader :files
55
+
56
+ def initialize(files = [])
57
+ @files = Array(files)
58
+ super()
59
+ end
60
+
61
+ def to_s
62
+ "Missing enterprise database dump(s): #{files.join(', ')}. " \
63
+ 'Run without --no-update so they can be downloaded from enterprise-data.wpscan.org, ' \
64
+ 'and check that your --enterprise-db-token is valid.'
65
+ end
66
+ end
43
67
  end
44
68
  end
data/lib/wpscan/scan.rb CHANGED
@@ -27,6 +27,7 @@ module WPScan
27
27
  # not secrets themselves, just references to files
28
28
  sensitive_args = %w[
29
29
  --api-token
30
+ --enterprise-db-token
30
31
  --http-auth
31
32
  --proxy-auth
32
33
  --cookie-string
@@ -95,7 +96,7 @@ module WPScan
95
96
 
96
97
  # Hook to be able to have an exit code returned
97
98
  # depending on the findings / errors
98
- # :nocov:
99
+ # simplecov:disable
99
100
  def exit_hook
100
101
  # Avoid registering this hook when the RSpec suite is running, otherwise it
101
102
  # can override RSpec's exit status and hide failed builds.
@@ -109,7 +110,7 @@ module WPScan
109
110
  exit(WPScan::ExitCode::OK)
110
111
  end
111
112
  end
112
- # :nocov:
113
+ # simplecov:enable
113
114
 
114
115
  def rspec_running?
115
116
  defined?(RSpec)
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Version
4
4
  module WPScan
5
- VERSION = '4.0.1'
5
+ VERSION = '4.1.0'
6
6
  end
@@ -56,13 +56,38 @@ module WPScan
56
56
  json_data['title'],
57
57
  references: references,
58
58
  type: json_data['vuln_type'],
59
- affected_versions: json_data['affected_versions'],
59
+ affected_versions: affected_versions_from(json_data),
60
60
  cvss: json_data['cvss']&.symbolize_keys,
61
61
  poc: json_data['poc'],
62
62
  uuid: json_data['id'].to_s # The 'id' field IS the UUID in WPScan API
63
63
  )
64
64
  end
65
65
 
66
+ # Reads the :affected_versions ranges used by both the API and the DB dumps.
67
+ # Legacy flat fixed_in / introduced_in entries are wrapped into a single range
68
+ # so they still get version-filtered instead of matching every version.
69
+ #
70
+ # @param [ Hash ] json_data
71
+ # @return [ Array<Hash>, nil ]
72
+ def self.affected_versions_from(json_data)
73
+ return json_data['affected_versions'] if json_data['affected_versions']
74
+ return nil unless json_data.key?('fixed_in') || json_data.key?('introduced_in')
75
+
76
+ [{ 'fixed_in' => json_data['fixed_in'], 'introduced_in' => json_data['introduced_in'] }]
77
+ end
78
+
79
+ # Orders entries oldest-first (created_at, updated_at, id) to match the API's
80
+ # order, so the API and the reverse-ordered DB dump report the same sequence.
81
+ # The trailing index keeps the sort stable when those fields are absent.
82
+ #
83
+ # @param [ Array<Hash> ] json_vulns Raw entries from the API or a DB dump
84
+ # @return [ Array<Hash> ]
85
+ def self.sort_json(json_vulns)
86
+ Array(json_vulns).each_with_index.sort_by do |vuln, index|
87
+ [vuln['created_at'].to_s, vuln['updated_at'].to_s, vuln['id'].to_s, index]
88
+ end.map(&:first)
89
+ end
90
+
66
91
  # Whether this vulnerability covers a given version. True when no
67
92
  # affected_versions are declared (advisory with no fix metadata) or when at
68
93
  # least one backported range covers the version.
@@ -45,11 +45,11 @@ module WPScan
45
45
  # @return [ Typhoeus::Response ]
46
46
  #
47
47
  # As webmock does not support redirects mocking, coverage is ignored
48
- # :nocov:
48
+ # simplecov:disable
49
49
  def homepage_res
50
50
  @homepage_res ||= WPScan::Browser.get_and_follow_location(url)
51
51
  end
52
- # :nocov:
52
+ # simplecov:enable
53
53
 
54
54
  # @return [ String ]
55
55
  def homepage_url
@@ -108,7 +108,7 @@ module WPScan
108
108
  # @return [ String ] The redirection url or nil
109
109
  #
110
110
  # As webmock does not support redirects mocking, coverage is ignored
111
- # :nocov:
111
+ # simplecov:disable
112
112
  def redirection(url = nil)
113
113
  url ||= @uri.to_s
114
114
 
@@ -118,7 +118,7 @@ module WPScan
118
118
 
119
119
  res.effective_url == url ? nil : res.effective_url
120
120
  end
121
- # :nocov:
121
+ # simplecov:enable
122
122
 
123
123
  # @return [ Hash ] The Typhoeus params to use to perform head requests
124
124
  def head_or_get_params
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wpscan
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - WPScanTeam
@@ -351,14 +351,14 @@ dependencies:
351
351
  requirements:
352
352
  - - "~>"
353
353
  - !ruby/object:Gem::Version
354
- version: 0.22.0
354
+ version: 1.0.0
355
355
  type: :development
356
356
  prerelease: false
357
357
  version_requirements: !ruby/object:Gem::Requirement
358
358
  requirements:
359
359
  - - "~>"
360
360
  - !ruby/object:Gem::Version
361
- version: 0.22.0
361
+ version: 1.0.0
362
362
  - !ruby/object:Gem::Dependency
363
363
  name: simplecov-lcov
364
364
  requirement: !ruby/object:Gem::Requirement