package-audit 0.6.2 → 0.7.1
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/lib/package/audit/formatter/version.rb +2 -0
- data/lib/package/audit/formatter/version_date.rb +2 -0
- data/lib/package/audit/npm/node_collection.rb +8 -6
- data/lib/package/audit/npm/npm_meta_data.rb +96 -32
- data/lib/package/audit/npm/vulnerability_finder.rb +22 -7
- data/lib/package/audit/npm/yarn_lock_parser.rb +143 -23
- data/lib/package/audit/ruby/gem_meta_data.rb +15 -2
- data/lib/package/audit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a14d75d3c6f64af8c327c492e0d0c1bcf98fc3119964e63d944fc33fc6ce6b8e
|
|
4
|
+
data.tar.gz: 01f5eaac43d53a65ac42a46a736793b8dbe56f4ad3531a8e649d9516b85a5027
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f2599769354017f84a5eb4f5da8cb6b67b69be12ed6f67126ed0701b1746461960c24ecd1ba7d2cf81d5719ca18234f1f9bdcb645a5bb9d8916faaca040ee96
|
|
7
|
+
data.tar.gz: 9eeeb933dd6e5535c0fdcdfca6fea339ca34fc1a611460b68ea0274da083ec08be63424eb82c7229ed939cf05fe49621bbf9f4d04af9abffc6001f7a43e507cb
|
|
@@ -12,6 +12,8 @@ module Package
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def format # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
15
|
+
return @curr if @target.nil? # No latest version available
|
|
16
|
+
|
|
15
17
|
version_parts = @curr.split('.').map(&:to_i)
|
|
16
18
|
latest_version_parts = @target.split('.').map(&:to_i)
|
|
17
19
|
curr_tokens = @curr.split('.')
|
|
@@ -55,15 +55,16 @@ module Package
|
|
|
55
55
|
private
|
|
56
56
|
|
|
57
57
|
def fetch_from_package_json
|
|
58
|
-
package_json = JSON.parse(File.read("#{@dir}/#{Const::File::PACKAGE_JSON}")
|
|
59
|
-
default_deps = package_json[
|
|
60
|
-
dev_deps = package_json[
|
|
58
|
+
package_json = JSON.parse(File.read("#{@dir}/#{Const::File::PACKAGE_JSON}"))
|
|
59
|
+
default_deps = package_json['dependencies'] || {}
|
|
60
|
+
dev_deps = package_json['devDependencies'] || {}
|
|
61
|
+
resolutions = package_json['resolutions'] || {}
|
|
61
62
|
|
|
62
63
|
# Filter out local dependencies before processing
|
|
63
64
|
default_deps = filter_local_dependencies(default_deps)
|
|
64
65
|
dev_deps = filter_local_dependencies(dev_deps)
|
|
65
66
|
|
|
66
|
-
[default_deps, dev_deps]
|
|
67
|
+
[default_deps, dev_deps, resolutions]
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def filter_local_dependencies(dependencies)
|
|
@@ -79,9 +80,10 @@ module Package
|
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
def fetch_from_lock_file
|
|
82
|
-
default_deps, dev_deps = fetch_from_package_json
|
|
83
|
+
default_deps, dev_deps, resolutions = fetch_from_package_json
|
|
83
84
|
if File.exist?("#{@dir}/#{Const::File::YARN_LOCK}")
|
|
84
|
-
YarnLockParser.new("#{@dir}/#{Const::File::YARN_LOCK}").fetch(default_deps || {}, dev_deps || {}
|
|
85
|
+
YarnLockParser.new("#{@dir}/#{Const::File::YARN_LOCK}").fetch(default_deps || {}, dev_deps || {},
|
|
86
|
+
resolutions || {})
|
|
85
87
|
else
|
|
86
88
|
[]
|
|
87
89
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'net/http'
|
|
3
|
+
require 'openssl'
|
|
3
4
|
require 'socket'
|
|
4
5
|
|
|
5
6
|
module Package
|
|
@@ -7,42 +8,27 @@ module Package
|
|
|
7
8
|
module Npm
|
|
8
9
|
class NpmMetaData
|
|
9
10
|
REGISTRY_URL = 'https://registry.npmjs.org'
|
|
11
|
+
BATCH_SIZE = 10 # Process 10 packages at a time
|
|
12
|
+
MAX_RETRIES = 3 # Maximum number of retries per request
|
|
13
|
+
INITIAL_RETRY_DELAY = 1 # Initial retry delay in seconds
|
|
14
|
+
TIMEOUT = 10 # Timeout in seconds
|
|
10
15
|
|
|
11
16
|
def initialize(packages)
|
|
12
17
|
@packages = packages
|
|
13
18
|
end
|
|
14
19
|
|
|
15
|
-
def fetch # rubocop:disable Metrics/
|
|
16
|
-
threads = @packages.map do |package|
|
|
17
|
-
Thread.new do
|
|
18
|
-
response = Net::HTTP.get_response(URI.parse("#{REGISTRY_URL}/#{package.name}"))
|
|
19
|
-
raise "Unable to fetch meta data for #{package.name} from #{REGISTRY_URL} (#{response.class})" unless
|
|
20
|
-
response.is_a?(Net::HTTPSuccess)
|
|
21
|
-
|
|
22
|
-
json_package = JSON.parse(response.body, symbolize_names: true)
|
|
23
|
-
update_meta_data(package, json_package)
|
|
24
|
-
rescue Net::TimeoutError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
25
|
-
warn "Warning: Network timeout while fetching metadata for #{package.name}: #{e.message}"
|
|
26
|
-
Thread.current[:exception] = e
|
|
27
|
-
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
|
28
|
-
warn "Warning: Network error while fetching metadata for #{package.name}: #{e.message}"
|
|
29
|
-
Thread.current[:exception] = e
|
|
30
|
-
rescue StandardError => e
|
|
31
|
-
Thread.current[:exception] = e
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
20
|
+
def fetch # rubocop:disable Metrics/MethodLength
|
|
35
21
|
network_errors = []
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
network_errors << thread[:exception]
|
|
43
|
-
else
|
|
44
|
-
raise thread[:exception]
|
|
22
|
+
|
|
23
|
+
@packages.each_slice(BATCH_SIZE) do |batch|
|
|
24
|
+
threads = batch.map do |package|
|
|
25
|
+
Thread.new do
|
|
26
|
+
fetch_package_metadata(package, network_errors)
|
|
27
|
+
end
|
|
45
28
|
end
|
|
29
|
+
|
|
30
|
+
threads.each(&:join)
|
|
31
|
+
sleep(0.1) # Small delay between batches to avoid overwhelming the server
|
|
46
32
|
end
|
|
47
33
|
|
|
48
34
|
unless network_errors.empty?
|
|
@@ -55,10 +41,88 @@ module Package
|
|
|
55
41
|
|
|
56
42
|
private
|
|
57
43
|
|
|
44
|
+
def fetch_package_metadata(package, network_errors, retry_count = 0)
|
|
45
|
+
response = make_request_with_retry(package.name, retry_count)
|
|
46
|
+
return if response.nil?
|
|
47
|
+
|
|
48
|
+
json_package = JSON.parse(response.body, symbolize_names: true)
|
|
49
|
+
update_meta_data(package, json_package)
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
handle_error(package, e, network_errors)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def make_request_with_retry(package_name, retry_count) # rubocop:disable Metrics/MethodLength
|
|
55
|
+
response = make_request(package_name)
|
|
56
|
+
return nil if response.is_a?(Net::HTTPNotFound) # Skip 404s - likely private packages
|
|
57
|
+
|
|
58
|
+
raise "Unable to fetch meta data for #{package_name} from #{REGISTRY_URL} (#{response.class})" unless
|
|
59
|
+
response.is_a?(Net::HTTPSuccess)
|
|
60
|
+
|
|
61
|
+
response
|
|
62
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
|
63
|
+
return nil if retry_count >= MAX_RETRIES
|
|
64
|
+
|
|
65
|
+
retry_after_delay(retry_count)
|
|
66
|
+
retry_count += 1
|
|
67
|
+
retry
|
|
68
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
69
|
+
warn "Warning: SSL verification failed for #{package_name}: #{e.message}"
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle_error(package, error, network_errors)
|
|
74
|
+
# Don't warn about 404s for private packages
|
|
75
|
+
return if error.is_a?(RuntimeError) && error.message.include?('(Net::HTTPNotFound)')
|
|
76
|
+
|
|
77
|
+
warn "Warning: Error while fetching metadata for #{package.name}: #{error.message}"
|
|
78
|
+
network_errors << error
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def make_request(package_name)
|
|
82
|
+
uri = URI(REGISTRY_URL)
|
|
83
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
84
|
+
http.use_ssl = true
|
|
85
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
86
|
+
http.cert_store = OpenSSL::X509::Store.new
|
|
87
|
+
http.cert_store.set_default_paths
|
|
88
|
+
http.read_timeout = TIMEOUT
|
|
89
|
+
http.open_timeout = TIMEOUT
|
|
90
|
+
|
|
91
|
+
path = "/#{package_name}"
|
|
92
|
+
http.get(path)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def retry_after_delay(retry_count)
|
|
96
|
+
delay = INITIAL_RETRY_DELAY * (2**retry_count) # Exponential backoff
|
|
97
|
+
sleep(delay)
|
|
98
|
+
end
|
|
99
|
+
|
|
58
100
|
def update_meta_data(package, json_data)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
101
|
+
# No early return - let's try to get metadata even if version is unknown
|
|
102
|
+
|
|
103
|
+
latest_version = find_latest_version(json_data)
|
|
104
|
+
return unless latest_version
|
|
105
|
+
|
|
106
|
+
dates = find_version_dates(json_data, package.version, latest_version)
|
|
107
|
+
return unless dates
|
|
108
|
+
|
|
109
|
+
update_package_metadata(package, latest_version, dates)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def find_latest_version(json_data)
|
|
113
|
+
json_data[:'dist-tags']&.[](:latest)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def find_version_dates(json_data, version, latest_version)
|
|
117
|
+
version_date = json_data[:time]&.[](version.to_sym)
|
|
118
|
+
latest_version_date = json_data[:time]&.[](latest_version.to_sym)
|
|
119
|
+
return unless version_date || latest_version_date # Return if both dates are missing
|
|
120
|
+
|
|
121
|
+
[version_date || latest_version_date, latest_version_date || version_date]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def update_package_metadata(package, latest_version, dates)
|
|
125
|
+
version_date, latest_version_date = dates
|
|
62
126
|
package.update version_date: Time.parse(version_date).strftime('%Y-%m-%d'),
|
|
63
127
|
latest_version: latest_version,
|
|
64
128
|
latest_version_date: Time.parse(latest_version_date).strftime('%Y-%m-%d')
|
|
@@ -32,17 +32,32 @@ module Package
|
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
-
def update_meta_data(json)
|
|
36
|
-
|
|
35
|
+
def update_meta_data(json)
|
|
36
|
+
resolution_path = json.dig(:data, :resolution, :path)
|
|
37
|
+
return unless resolution_path # Skip if no resolution path (private package)
|
|
38
|
+
|
|
39
|
+
parent_name = resolution_path.split('>').first
|
|
37
40
|
advisory = json[:data][:advisory]
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
package_info = extract_package_info(advisory)
|
|
42
|
+
update_vulnerability_info(package_info, parent_name, advisory[:severity])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def extract_package_info(advisory)
|
|
46
|
+
{
|
|
47
|
+
name: advisory[:module_name],
|
|
48
|
+
version: advisory[:findings][0][:version]
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def update_vulnerability_info(package_info, parent_name, severity)
|
|
53
|
+
name = package_info[:name]
|
|
54
|
+
version = package_info[:version]
|
|
40
55
|
full_name = "#{name}@#{version}"
|
|
41
|
-
vulnerability =
|
|
56
|
+
vulnerability = severity || Enum::VulnerabilityType::UNKNOWN
|
|
42
57
|
|
|
43
|
-
@vuln_hash[full_name]
|
|
58
|
+
@vuln_hash[full_name] ||= Package.new(name, version, 'node')
|
|
44
59
|
@vuln_hash[full_name].update vulnerabilities: @vuln_hash[full_name].vulnerabilities + [vulnerability]
|
|
45
|
-
@vuln_hash[full_name].update groups: @pkg_hash[parent_name]
|
|
60
|
+
@vuln_hash[full_name].update groups: @pkg_hash[parent_name]&.groups&.map(&:to_s) || []
|
|
46
61
|
end
|
|
47
62
|
end
|
|
48
63
|
end
|
|
@@ -9,45 +9,127 @@ module Package
|
|
|
9
9
|
@yarn_lock_path = yarn_lock_path
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def fetch(default_deps, dev_deps
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
pkg_block = fetch_package_block(dep_name, expected_version)
|
|
16
|
-
version = fetch_package_version(dep_name, pkg_block)
|
|
17
|
-
pks = Package.new(dep_name.to_s, version, 'node')
|
|
18
|
-
pks.update groups: if dev_deps.key?(dep_name)
|
|
19
|
-
[Enum::Group::DEV]
|
|
20
|
-
else
|
|
21
|
-
[Enum::Group::DEFAULT, Enum::Group::DEV]
|
|
22
|
-
end
|
|
23
|
-
pkgs << pks
|
|
12
|
+
def fetch(default_deps, dev_deps, resolutions = {})
|
|
13
|
+
default_deps.merge(dev_deps).map do |dep_name, expected_version|
|
|
14
|
+
process_package(dep_name, expected_version, dev_deps, resolutions)
|
|
24
15
|
end
|
|
25
|
-
pkgs
|
|
26
16
|
end
|
|
27
17
|
|
|
28
18
|
private
|
|
29
19
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
def process_package(dep_name, expected_version, dev_deps, resolutions)
|
|
21
|
+
version_to_check = get_version_to_check(dep_name, expected_version, resolutions)
|
|
22
|
+
pkg_block = fetch_package_block(dep_name, version_to_check)
|
|
23
|
+
version = fetch_package_version(dep_name, pkg_block)
|
|
24
|
+
create_package(dep_name, version, dev_deps)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_version_to_check(dep_name, expected_version, resolutions)
|
|
28
|
+
version_to_check = resolutions[dep_name] || expected_version
|
|
29
|
+
return version_to_check unless version_to_check.start_with?('patch:')
|
|
30
|
+
|
|
31
|
+
patch_version = version_to_check.match(/patch:.*?@npm%3A([\d.-]+)#/)&.captures&.[](0)
|
|
32
|
+
patch_version || version_to_check
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_package(dep_name, version, dev_deps)
|
|
36
|
+
pks = Package.new(dep_name.to_s, version, 'node')
|
|
37
|
+
pks.update groups: package_groups(dep_name, dev_deps)
|
|
38
|
+
pks
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def package_groups(dep_name, dev_deps)
|
|
42
|
+
if dev_deps.key?(dep_name)
|
|
43
|
+
[Enum::Group::DEV]
|
|
44
|
+
else
|
|
45
|
+
[Enum::Group::DEFAULT, Enum::Group::DEV]
|
|
35
46
|
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fetch_package_block(dep_name, expected_version)
|
|
50
|
+
blocks = find_package_blocks(dep_name)
|
|
51
|
+
raise NoMatchingPatternError, "Unable to find \"#{dep_name}\" in #{@yarn_lock_path}" if blocks.empty?
|
|
52
|
+
|
|
53
|
+
find_matching_block(blocks, dep_name, expected_version)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def find_package_blocks(dep_name)
|
|
57
|
+
block_pattern = build_block_pattern(dep_name)
|
|
58
|
+
@yarn_lock_file.scan(block_pattern)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_block_pattern(dep_name)
|
|
62
|
+
/
|
|
63
|
+
^["']? # Start of line with optional quote
|
|
64
|
+
(?:[^"\n]+,\s*)* # Any previous entries in a comma-separated list
|
|
65
|
+
(?:patch:)? # Optional patch prefix
|
|
66
|
+
#{Regexp.escape(dep_name)}@[^:\n]+ # Our package name and version
|
|
67
|
+
(?:[^"\n]*,\s*[^"\n]+)* # Any following entries
|
|
68
|
+
["']?:.*? # End quote and colon, followed by the rest
|
|
69
|
+
(?:resolution:.*?)? # Optional resolution field
|
|
70
|
+
(?=\n["']|\n\s*\n|\z) # Until next entry or end of file
|
|
71
|
+
/mx
|
|
72
|
+
end
|
|
36
73
|
|
|
37
|
-
|
|
74
|
+
def find_matching_block(blocks, dep_name, expected_version)
|
|
75
|
+
version_pattern = build_version_pattern(dep_name, expected_version)
|
|
76
|
+
blocks.find { |block| block.match?(version_pattern) } || blocks.first
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_version_pattern(dep_name, expected_version)
|
|
80
|
+
/
|
|
81
|
+
(?:patch:)?#{Regexp.escape(dep_name)}@
|
|
82
|
+
(?:npm:)?#{Regexp.escape(expected_version)}["']?(?:,|:)
|
|
83
|
+
/x
|
|
38
84
|
end
|
|
39
85
|
|
|
40
86
|
def fetch_package_version(dep_name, pkg_block)
|
|
41
|
-
|
|
87
|
+
# Try different version formats:
|
|
88
|
+
# 1. version: "1.2.3" - quoted version
|
|
89
|
+
# 2. version: 1.2.3 - unquoted version
|
|
90
|
+
# 3. "pkg@1.2.3": - version in package spec
|
|
91
|
+
# 4. "pkg@npm:1.2.3": - version with npm prefix
|
|
92
|
+
# Try to find version in this order:
|
|
93
|
+
# 1. resolution field (for overrides)
|
|
94
|
+
# 2. version field (both quoted and unquoted)
|
|
95
|
+
# 3. package spec
|
|
96
|
+
version = extract_version_from_block(dep_name, pkg_block)
|
|
97
|
+
|
|
42
98
|
if version.nil?
|
|
43
99
|
raise NoMatchingPatternError,
|
|
44
100
|
"Unable to find the version of \"#{dep_name}\" in #{@yarn_lock_path}"
|
|
45
101
|
end
|
|
46
102
|
|
|
47
|
-
version || '
|
|
103
|
+
version || 'unknown'
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_version_from_block(dep_name, pkg_block)
|
|
107
|
+
find_resolution_version(dep_name, pkg_block) ||
|
|
108
|
+
find_version_field(pkg_block) ||
|
|
109
|
+
find_spec_version(dep_name, pkg_block)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def find_resolution_version(dep_name, pkg_block)
|
|
113
|
+
pattern = /
|
|
114
|
+
resolution:.*?["']#{Regexp.escape(dep_name)}@
|
|
115
|
+
(?:npm:)?(?:patch:#{Regexp.escape(dep_name)}@npm%3A)?
|
|
116
|
+
([\d.-]+(?:-(?:beta|rc|dev)\.\d+(?:\.\d+)?)?)
|
|
117
|
+
(?:&hash=[a-f0-9]+)?["']
|
|
118
|
+
/x
|
|
119
|
+
pkg_block.match(pattern)&.captures&.[](0)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def find_version_field(pkg_block)
|
|
123
|
+
pattern = /version["']?\s*["']?([\d.-]+(?:-(?:beta|rc|dev)\.\d+(?:\.\d+)?)?)(?:&hash=[a-f0-9]+)?["']?(?:\s|$)/
|
|
124
|
+
pkg_block.match(pattern)&.captures&.[](0)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def find_spec_version(dep_name, pkg_block)
|
|
128
|
+
pattern = %r{^.*?#{Regexp.escape(dep_name)}@(?:npm:|https://[^#]+#)?(?:patch:#{Regexp.escape(dep_name)}@npm%3A)?(?:v)?([\d.-]+(?:-(?:beta|rc|dev)\.\d+(?:\.\d+)?)?)(?:&hash=[a-f0-9]+)?["']?(?:,|\s*:)}m
|
|
129
|
+
pkg_block.match(pattern)&.captures&.[](0)
|
|
48
130
|
end
|
|
49
131
|
|
|
50
|
-
def regex_pattern_for_package(dep_name,
|
|
132
|
+
def regex_pattern_for_package(dep_name, _version)
|
|
51
133
|
# assume the package name is prefixed by a space, a quote or be the first thing on the line
|
|
52
134
|
# there can be multiple comma-separated versions on the same line with or without quotes
|
|
53
135
|
# Here are some examples of strings that would be matched:
|
|
@@ -55,7 +137,45 @@ module Package
|
|
|
55
137
|
# - lodash@^4.17.15, lodash@^4.17.20:
|
|
56
138
|
# - "@adobe/css-tools@^4.0.1":
|
|
57
139
|
# - "@babel/runtime@^7.23.1", "@babel/runtime@^7.9.2":
|
|
58
|
-
|
|
140
|
+
# For resolutions (exact versions):
|
|
141
|
+
# - "@apollo/client@3.12.5":
|
|
142
|
+
# For both regular dependencies and resolutions
|
|
143
|
+
# The package might appear in different formats:
|
|
144
|
+
# 1. As a dependency spec: "@apollo/client@^3.14.0"
|
|
145
|
+
# 2. As a resolved version: "@apollo/client@3.12.5"
|
|
146
|
+
# 3. As part of a multi-version spec: "@apollo/client@^3.14.0, @apollo/client@^3.12.5"
|
|
147
|
+
# 4. With npm prefix: "@apollo/client@npm:3.12.5"
|
|
148
|
+
# 5. In resolution field: "resolution: \"@apollo/client@npm:3.12.5\""
|
|
149
|
+
escaped_name = Regexp.escape(dep_name)
|
|
150
|
+
# Look for any entry that starts with our package name and includes the version
|
|
151
|
+
# We match the entire block and let fetch_package_version handle version extraction
|
|
152
|
+
# The pattern matches:
|
|
153
|
+
# 1. Package name at start of line or after quote
|
|
154
|
+
# 2. Everything up to the next double newline or end of file
|
|
155
|
+
# 3. Handles both compact and expanded formats with dependencies
|
|
156
|
+
# Match both old and new yarn.lock formats:
|
|
157
|
+
# Old: pkg@^1.0.0:
|
|
158
|
+
# New: "pkg@^1.0.0":
|
|
159
|
+
# The pattern matches the entire block including any indented lines
|
|
160
|
+
# We look for:
|
|
161
|
+
# 1. Start of line
|
|
162
|
+
# 2. Optional quote
|
|
163
|
+
# 3. Package name
|
|
164
|
+
# 4. @ symbol
|
|
165
|
+
# 5. Version spec (anything up to : or ")
|
|
166
|
+
# 6. Optional quote and colon
|
|
167
|
+
# 7. Rest of the block until next entry or end of file
|
|
168
|
+
# The pattern needs to match:
|
|
169
|
+
# 1. Basic format: pkg@^1.0.0:
|
|
170
|
+
# 2. Quoted format: "pkg@^1.0.0":
|
|
171
|
+
# 3. Multiple specs: "pkg@1.0.0", "pkg@npm:1.0.0":
|
|
172
|
+
# 4. Scoped packages: "@scope/pkg@1.0.0":
|
|
173
|
+
# Match any of:
|
|
174
|
+
# 1. Basic: pkg@^1.0.0:
|
|
175
|
+
# 2. Quoted: "pkg@^1.0.0":
|
|
176
|
+
# 3. Multiple: "pkg@1.0.0", "pkg@npm:1.0.0":
|
|
177
|
+
# 4. Scoped: "@scope/pkg@1.0.0":
|
|
178
|
+
/^(?:["']?#{escaped_name}@[^,\n:"]+(?:,\s*["']#{escaped_name}@[^,\n:"]+)*["']?:.*?)(?=\n["']|\n\s*\n|\z)/m
|
|
59
179
|
end
|
|
60
180
|
end
|
|
61
181
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative '../models/package'
|
|
2
|
+
require 'openssl'
|
|
2
3
|
|
|
3
4
|
module Package
|
|
4
5
|
module Audit
|
|
@@ -170,15 +171,24 @@ module Package
|
|
|
170
171
|
}
|
|
171
172
|
end
|
|
172
173
|
|
|
173
|
-
def fetch_gem_version_dates(gem_name)
|
|
174
|
+
def fetch_gem_version_dates(gem_name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
174
175
|
uri = build_api_uri(gem_name)
|
|
175
176
|
response = make_http_request(uri)
|
|
176
177
|
|
|
177
178
|
return nil unless success_response?(response)
|
|
178
179
|
|
|
179
180
|
parse_version_dates(response.body)
|
|
181
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
182
|
+
log_api_error(gem_name, "Network timeout: #{e.message}") if debug_mode?
|
|
183
|
+
nil
|
|
184
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
185
|
+
log_api_error(gem_name, "SSL verification failed: #{e.message}") if debug_mode?
|
|
186
|
+
nil
|
|
187
|
+
rescue SocketError, Errno::ECONNREFUSED => e
|
|
188
|
+
log_api_error(gem_name, "Network error: #{e.message}") if debug_mode?
|
|
189
|
+
nil
|
|
180
190
|
rescue StandardError => e
|
|
181
|
-
log_api_error(gem_name, e) if debug_mode?
|
|
191
|
+
log_api_error(gem_name, e.message) if debug_mode?
|
|
182
192
|
nil
|
|
183
193
|
end
|
|
184
194
|
|
|
@@ -206,6 +216,9 @@ module Package
|
|
|
206
216
|
def create_http_client(uri)
|
|
207
217
|
Net::HTTP.new(uri.host, uri.port).tap do |http|
|
|
208
218
|
http.use_ssl = true
|
|
219
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
220
|
+
http.cert_store = OpenSSL::X509::Store.new
|
|
221
|
+
http.cert_store.set_default_paths
|
|
209
222
|
http.read_timeout = HTTP_READ_TIMEOUT
|
|
210
223
|
http.open_timeout = HTTP_OPEN_TIMEOUT
|
|
211
224
|
end
|