package-audit 0.6.1 → 0.6.2
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/ruby/gem_meta_data.rb +217 -27
- data/lib/package/audit/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cca016948e4ab3d4643c8e7c7b6bca064873a0b390709d6318d899ee03464c36
|
|
4
|
+
data.tar.gz: '037543298033670ef8cd62ceb31a021389e55b0baf634117777018933d89b6b3'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 494ad5afd212b57e596fd599b8b57f86453c3cb5f988d4ae88a572c685a83f8ba4c39c750353e962453d98ba3336d30ecf1da729ffad4c26cfce418c049e56eb
|
|
7
|
+
data.tar.gz: 6088d06e5a5ccbf73d9ecd3e3fbcd130030dbc47ca861a910f56ba21850ae467eda5f7a4b0064200d18bb1d7fe461f7db561f64f471b0c747a25d6bc18ec9115
|
|
@@ -3,7 +3,16 @@ require_relative '../models/package'
|
|
|
3
3
|
module Package
|
|
4
4
|
module Audit
|
|
5
5
|
module Ruby
|
|
6
|
-
class GemMetaData
|
|
6
|
+
class GemMetaData # rubocop:disable Metrics/ClassLength
|
|
7
|
+
# API and timeout constants
|
|
8
|
+
RUBYGEMS_API_BASE = 'https://rubygems.org/api/v1/versions'
|
|
9
|
+
HTTP_READ_TIMEOUT = 10
|
|
10
|
+
HTTP_OPEN_TIMEOUT = 5
|
|
11
|
+
PLACEHOLDER_DATE_THRESHOLD = 1980
|
|
12
|
+
DEFAULT_DATE_FORMAT = '%Y-%m-%d'
|
|
13
|
+
EPOCH_TIME = Time.new(0)
|
|
14
|
+
INITIAL_VERSION = Gem::Version.new('0.0.0.0')
|
|
15
|
+
|
|
7
16
|
def initialize(dir, pkgs)
|
|
8
17
|
@dir = dir
|
|
9
18
|
@pkgs = pkgs
|
|
@@ -18,45 +27,226 @@ module Package
|
|
|
18
27
|
|
|
19
28
|
private
|
|
20
29
|
|
|
21
|
-
def find_rubygems_metadata
|
|
30
|
+
def find_rubygems_metadata
|
|
31
|
+
# Performance-optimized approach:
|
|
32
|
+
# 1. Use fast local SpecFetcher for version numbers and dates
|
|
33
|
+
# 2. Only make HTTP API calls for gems with placeholder dates (1980-01-02)
|
|
34
|
+
# 3. This avoids network calls for gems with proper local date metadata
|
|
22
35
|
fetcher = Gem::SpecFetcher.fetcher
|
|
36
|
+
gems_needing_api_lookup = []
|
|
23
37
|
|
|
24
38
|
@pkgs.each do |pkg|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
latest_version_date = Time.new(0)
|
|
28
|
-
local_version = Gem::Version.new(pkg.version)
|
|
29
|
-
latest_version = Gem::Version.new('0.0.0.0')
|
|
39
|
+
result = process_package_metadata(pkg, fetcher)
|
|
40
|
+
next unless result
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
if result[:needs_api_lookup]
|
|
43
|
+
gems_needing_api_lookup << result[:gem_data]
|
|
44
|
+
else
|
|
45
|
+
update_package_with_local_dates(pkg, result[:metadata])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
# Batch API lookups only for gems with placeholder dates
|
|
50
|
+
process_api_lookups(gems_needing_api_lookup) unless gems_needing_api_lookup.empty?
|
|
51
|
+
end
|
|
34
52
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
def needs_api_lookup?(date)
|
|
54
|
+
return true if date.nil?
|
|
55
|
+
|
|
56
|
+
date.year <= PLACEHOLDER_DATE_THRESHOLD
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def process_package_metadata(pkg, fetcher)
|
|
60
|
+
gem_dependency = Gem::Dependency.new pkg.name, ">= #{pkg.version}"
|
|
61
|
+
remote_dependencies, = fetcher.spec_for_dependency gem_dependency
|
|
62
|
+
return nil unless remote_dependencies.any?
|
|
63
|
+
|
|
64
|
+
metadata = extract_local_metadata(pkg, remote_dependencies)
|
|
65
|
+
needs_lookup = needs_api_lookup?(metadata[:local_version_date]) ||
|
|
66
|
+
needs_api_lookup?(metadata[:latest_version_date])
|
|
40
67
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
{
|
|
69
|
+
needs_api_lookup: needs_lookup,
|
|
70
|
+
metadata: metadata,
|
|
71
|
+
gem_data: needs_lookup ? build_gem_data(pkg, metadata) : nil
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extract_local_metadata(pkg, remote_dependencies)
|
|
76
|
+
metadata = initialize_metadata_defaults(pkg)
|
|
77
|
+
|
|
78
|
+
remote_dependencies.each do |remote_spec, _|
|
|
79
|
+
update_version_info(metadata, remote_spec)
|
|
45
80
|
end
|
|
81
|
+
|
|
82
|
+
metadata
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def initialize_metadata_defaults(pkg)
|
|
86
|
+
{
|
|
87
|
+
local_version_date: EPOCH_TIME,
|
|
88
|
+
latest_version_date: EPOCH_TIME,
|
|
89
|
+
local_version: Gem::Version.new(pkg.version),
|
|
90
|
+
latest_version: INITIAL_VERSION
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def update_version_info(metadata, remote_spec)
|
|
95
|
+
metadata[:latest_version] = remote_spec.version if metadata[:latest_version] < remote_spec.version
|
|
96
|
+
|
|
97
|
+
metadata[:latest_version_date] = remote_spec.date if metadata[:latest_version_date] < remote_spec.date
|
|
98
|
+
|
|
99
|
+
return unless metadata[:local_version] == remote_spec.version
|
|
100
|
+
|
|
101
|
+
metadata[:local_version_date] = remote_spec.date
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_gem_data(pkg, metadata)
|
|
105
|
+
{
|
|
106
|
+
pkg: pkg,
|
|
107
|
+
latest_version: metadata[:latest_version].to_s,
|
|
108
|
+
local_version_date: metadata[:local_version_date],
|
|
109
|
+
latest_version_date: metadata[:latest_version_date]
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def update_package_with_local_dates(pkg, metadata)
|
|
114
|
+
store_package(pkg)
|
|
115
|
+
pkg.update(
|
|
116
|
+
latest_version: metadata[:latest_version].to_s,
|
|
117
|
+
version_date: format_time(metadata[:local_version_date]),
|
|
118
|
+
latest_version_date: format_time(metadata[:latest_version_date])
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def store_package(pkg)
|
|
123
|
+
@gem_hash[pkg.name] = pkg
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def format_time(time)
|
|
127
|
+
time.strftime(DEFAULT_DATE_FORMAT)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def process_api_lookups(gem_data_array)
|
|
131
|
+
gem_data_array.each { |gem_data| process_single_api_lookup(gem_data) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def process_single_api_lookup(gem_data) # rubocop:disable Metrics/MethodLength
|
|
135
|
+
pkg = gem_data[:pkg]
|
|
136
|
+
version_dates = fetch_gem_version_dates(pkg.name)
|
|
137
|
+
final_dates = determine_final_dates(
|
|
138
|
+
version_dates,
|
|
139
|
+
pkg.version,
|
|
140
|
+
gem_data[:latest_version],
|
|
141
|
+
gem_data[:local_version_date],
|
|
142
|
+
gem_data[:latest_version_date]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
store_package(pkg)
|
|
146
|
+
pkg.update(
|
|
147
|
+
latest_version: gem_data[:latest_version],
|
|
148
|
+
version_date: final_dates[:local],
|
|
149
|
+
latest_version_date: final_dates[:latest]
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def determine_final_dates(version_dates, local_version, latest_version, local_date, latest_date)
|
|
154
|
+
return fallback_to_local_dates(local_date, latest_date) unless version_dates
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
local: format_date(resolve_date(version_dates[local_version], local_date)),
|
|
158
|
+
latest: format_date(resolve_date(version_dates[latest_version], latest_date))
|
|
159
|
+
}
|
|
46
160
|
end
|
|
47
161
|
|
|
48
|
-
def
|
|
162
|
+
def resolve_date(api_date, fallback_date)
|
|
163
|
+
api_date || (needs_api_lookup?(fallback_date) ? nil : fallback_date)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def fallback_to_local_dates(local_date, latest_date)
|
|
167
|
+
{
|
|
168
|
+
local: local_date.strftime(DEFAULT_DATE_FORMAT),
|
|
169
|
+
latest: latest_date.strftime(DEFAULT_DATE_FORMAT)
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def fetch_gem_version_dates(gem_name)
|
|
174
|
+
uri = build_api_uri(gem_name)
|
|
175
|
+
response = make_http_request(uri)
|
|
176
|
+
|
|
177
|
+
return nil unless success_response?(response)
|
|
178
|
+
|
|
179
|
+
parse_version_dates(response.body)
|
|
180
|
+
rescue StandardError => e
|
|
181
|
+
log_api_error(gem_name, e) if debug_mode?
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def build_api_uri(gem_name)
|
|
186
|
+
URI("#{RUBYGEMS_API_BASE}/#{gem_name}.json")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def make_http_request(uri)
|
|
190
|
+
http = create_http_client(uri)
|
|
191
|
+
http.request(Net::HTTP::Get.new(uri))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def success_response?(response)
|
|
195
|
+
response.code == '200'
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def debug_mode?
|
|
199
|
+
ENV.fetch('DEBUG', nil)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def log_api_error(gem_name, error)
|
|
203
|
+
warn "Warning: Failed to fetch version dates for #{gem_name}: #{error.message}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def create_http_client(uri)
|
|
207
|
+
Net::HTTP.new(uri.host, uri.port).tap do |http|
|
|
208
|
+
http.use_ssl = true
|
|
209
|
+
http.read_timeout = HTTP_READ_TIMEOUT
|
|
210
|
+
http.open_timeout = HTTP_OPEN_TIMEOUT
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def parse_version_dates(response_body)
|
|
215
|
+
versions = JSON.parse(response_body)
|
|
216
|
+
versions.each_with_object({}) do |version_info, dates|
|
|
217
|
+
dates[version_info['number']] = version_info['created_at']
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def format_date(date_string)
|
|
222
|
+
return 'N/A' if date_string.nil?
|
|
223
|
+
|
|
224
|
+
Time.parse(date_string).strftime(DEFAULT_DATE_FORMAT)
|
|
225
|
+
rescue StandardError
|
|
226
|
+
'N/A'
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def assign_groups
|
|
230
|
+
definition = build_bundler_definition
|
|
231
|
+
groups = definition.groups.uniq.sort
|
|
232
|
+
groups.each { |group| update_gem_groups(definition, group) }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def build_bundler_definition
|
|
49
236
|
definition = Bundler::Definition.build Pathname("#{@dir}/Gemfile"), Pathname("#{@dir}/Gemfile.lock"), nil
|
|
50
237
|
Bundler.ui.level = 'error'
|
|
51
238
|
definition.resolve_remotely!
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
239
|
+
definition
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def update_gem_groups(definition, group)
|
|
243
|
+
specs = definition.specs_for([group])
|
|
244
|
+
specs.each do |spec|
|
|
245
|
+
next unless @gem_hash.key?(spec.name)
|
|
246
|
+
|
|
247
|
+
current_groups = @gem_hash[spec.name].groups
|
|
248
|
+
updated_groups = (current_groups | [group]).map(&:to_s)
|
|
249
|
+
@gem_hash[spec.name].update(groups: updated_groups)
|
|
60
250
|
end
|
|
61
251
|
end
|
|
62
252
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: package-audit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vadim Kononov
|
|
@@ -111,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
111
111
|
- !ruby/object:Gem::Version
|
|
112
112
|
version: '0'
|
|
113
113
|
requirements: []
|
|
114
|
-
rubygems_version: 3.6.
|
|
114
|
+
rubygems_version: 3.6.9
|
|
115
115
|
specification_version: 4
|
|
116
116
|
summary: A helper tool to find outdated, deprecated and vulnerable dependencies.
|
|
117
117
|
test_files: []
|