git-pkgs 0.6.2 → 0.7.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/.gitattributes +28 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +15 -0
- data/Dockerfile +18 -0
- data/Formula/git-pkgs.rb +28 -0
- data/README.md +36 -4
- data/lib/git/pkgs/analyzer.rb +141 -9
- data/lib/git/pkgs/cli.rb +16 -6
- data/lib/git/pkgs/commands/blame.rb +0 -18
- data/lib/git/pkgs/commands/diff.rb +122 -5
- data/lib/git/pkgs/commands/diff_driver.rb +24 -4
- data/lib/git/pkgs/commands/init.rb +5 -0
- data/lib/git/pkgs/commands/list.rb +60 -15
- data/lib/git/pkgs/commands/show.rb +126 -3
- data/lib/git/pkgs/commands/stale.rb +6 -2
- data/lib/git/pkgs/commands/update.rb +3 -0
- data/lib/git/pkgs/commands/vulns/base.rb +354 -0
- data/lib/git/pkgs/commands/vulns/blame.rb +276 -0
- data/lib/git/pkgs/commands/vulns/diff.rb +172 -0
- data/lib/git/pkgs/commands/vulns/exposure.rb +418 -0
- data/lib/git/pkgs/commands/vulns/history.rb +345 -0
- data/lib/git/pkgs/commands/vulns/log.rb +218 -0
- data/lib/git/pkgs/commands/vulns/praise.rb +238 -0
- data/lib/git/pkgs/commands/vulns/scan.rb +231 -0
- data/lib/git/pkgs/commands/vulns/show.rb +216 -0
- data/lib/git/pkgs/commands/vulns/sync.rb +108 -0
- data/lib/git/pkgs/commands/vulns.rb +50 -0
- data/lib/git/pkgs/config.rb +8 -1
- data/lib/git/pkgs/database.rb +135 -5
- data/lib/git/pkgs/ecosystems.rb +83 -0
- data/lib/git/pkgs/models/package.rb +54 -0
- data/lib/git/pkgs/models/vulnerability.rb +300 -0
- data/lib/git/pkgs/models/vulnerability_package.rb +59 -0
- data/lib/git/pkgs/osv_client.rb +151 -0
- data/lib/git/pkgs/output.rb +22 -0
- data/lib/git/pkgs/version.rb +1 -1
- data/lib/git/pkgs.rb +6 -0
- metadata +66 -4
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
module Vulns
|
|
7
|
+
class Sync
|
|
8
|
+
include Base
|
|
9
|
+
|
|
10
|
+
def initialize(args)
|
|
11
|
+
@args = args.dup
|
|
12
|
+
@options = parse_options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse_options
|
|
16
|
+
options = {}
|
|
17
|
+
|
|
18
|
+
parser = OptionParser.new do |opts|
|
|
19
|
+
opts.banner = "Usage: git pkgs vulns sync [options]"
|
|
20
|
+
opts.separator ""
|
|
21
|
+
opts.separator "Sync vulnerability data from OSV."
|
|
22
|
+
opts.separator ""
|
|
23
|
+
opts.separator "Options:"
|
|
24
|
+
|
|
25
|
+
opts.on("--refresh", "Force refresh even if cache is recent") do
|
|
26
|
+
options[:refresh] = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
opts.on("-h", "--help", "Show this help") do
|
|
30
|
+
puts opts
|
|
31
|
+
exit
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
parser.parse!(@args)
|
|
36
|
+
options
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run
|
|
40
|
+
repo = Repository.new
|
|
41
|
+
|
|
42
|
+
unless Database.exists?(repo.git_dir)
|
|
43
|
+
error "No database found. Run 'git pkgs init' first."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Database.connect(repo.git_dir)
|
|
47
|
+
|
|
48
|
+
packages = Models::Package.all
|
|
49
|
+
if packages.empty?
|
|
50
|
+
info "No packages to sync. Run 'git pkgs vulns' first to populate packages."
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
stale_packages = packages.select(&:needs_vuln_sync?)
|
|
55
|
+
|
|
56
|
+
if stale_packages.empty? && !@options[:refresh]
|
|
57
|
+
info "All packages up to date. Use --refresh to force update."
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
packages_to_sync = @options[:refresh] ? packages : stale_packages
|
|
62
|
+
|
|
63
|
+
info "Syncing vulnerabilities for #{packages_to_sync.count} packages..."
|
|
64
|
+
|
|
65
|
+
client = OsvClient.new
|
|
66
|
+
synced = 0
|
|
67
|
+
vuln_count = 0
|
|
68
|
+
|
|
69
|
+
packages_to_sync.each_slice(100) do |batch|
|
|
70
|
+
queries = batch.map do |pkg|
|
|
71
|
+
osv_ecosystem = Ecosystems.to_osv(pkg.ecosystem)
|
|
72
|
+
next unless osv_ecosystem
|
|
73
|
+
|
|
74
|
+
{ ecosystem: osv_ecosystem, name: pkg.name }
|
|
75
|
+
end.compact
|
|
76
|
+
|
|
77
|
+
results = client.query_batch(queries)
|
|
78
|
+
|
|
79
|
+
# Collect all unique vuln IDs from this batch to fetch full details
|
|
80
|
+
vuln_ids = results.flatten.map { |v| v["id"] }.uniq
|
|
81
|
+
|
|
82
|
+
# Fetch full vulnerability details and create records
|
|
83
|
+
vuln_ids.each do |vuln_id|
|
|
84
|
+
existing = Models::Vulnerability.first(id: vuln_id)
|
|
85
|
+
next if existing&.vulnerability_packages&.any? && !@options[:refresh]
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
full_vuln = client.get_vulnerability(vuln_id)
|
|
89
|
+
Models::Vulnerability.from_osv(full_vuln)
|
|
90
|
+
vuln_count += 1
|
|
91
|
+
rescue OsvClient::ApiError
|
|
92
|
+
# Skip vulnerabilities we can't fetch
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
batch.each do |pkg|
|
|
97
|
+
pkg.mark_vulns_synced
|
|
98
|
+
synced += 1
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
info "Synced #{synced} packages, found #{vuln_count} vulnerability records."
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "vulns/base"
|
|
4
|
+
require_relative "vulns/scan"
|
|
5
|
+
require_relative "vulns/sync"
|
|
6
|
+
require_relative "vulns/blame"
|
|
7
|
+
require_relative "vulns/praise"
|
|
8
|
+
require_relative "vulns/exposure"
|
|
9
|
+
require_relative "vulns/diff"
|
|
10
|
+
require_relative "vulns/log"
|
|
11
|
+
require_relative "vulns/history"
|
|
12
|
+
require_relative "vulns/show"
|
|
13
|
+
|
|
14
|
+
module Git
|
|
15
|
+
module Pkgs
|
|
16
|
+
module Commands
|
|
17
|
+
class VulnsCommand
|
|
18
|
+
SUBCOMMANDS = %w[sync blame praise exposure diff log history show].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(args)
|
|
21
|
+
@args = args.dup
|
|
22
|
+
@subcommand = detect_subcommand
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def detect_subcommand
|
|
26
|
+
return nil if @args.empty?
|
|
27
|
+
return nil unless SUBCOMMANDS.include?(@args.first)
|
|
28
|
+
|
|
29
|
+
@args.shift
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run
|
|
33
|
+
handler_class = case @subcommand
|
|
34
|
+
when "sync" then Vulns::Sync
|
|
35
|
+
when "blame" then Vulns::Blame
|
|
36
|
+
when "praise" then Vulns::Praise
|
|
37
|
+
when "exposure" then Vulns::Exposure
|
|
38
|
+
when "diff" then Vulns::Diff
|
|
39
|
+
when "log" then Vulns::Log
|
|
40
|
+
when "history" then Vulns::History
|
|
41
|
+
when "show" then Vulns::Show
|
|
42
|
+
else Vulns::Scan
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
handler_class.new(@args).run
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/git/pkgs/config.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bibliothecary"
|
|
4
|
+
require "open3"
|
|
4
5
|
|
|
5
6
|
module Git
|
|
6
7
|
module Pkgs
|
|
@@ -52,7 +53,13 @@ module Git
|
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def self.read_config_list(key)
|
|
55
|
-
|
|
56
|
+
args = if Git::Pkgs.work_tree
|
|
57
|
+
["git", "-C", Git::Pkgs.work_tree.to_s, "config", "--get-all", key.to_s]
|
|
58
|
+
else
|
|
59
|
+
["git", "config", "--get-all", key.to_s]
|
|
60
|
+
end
|
|
61
|
+
stdout, _stderr, _status = Open3.capture3(*args)
|
|
62
|
+
stdout.split("\n").map(&:strip).reject(&:empty?)
|
|
56
63
|
end
|
|
57
64
|
end
|
|
58
65
|
end
|
data/lib/git/pkgs/database.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Git
|
|
|
15
15
|
module Pkgs
|
|
16
16
|
class Database
|
|
17
17
|
DB_FILE = "pkgs.sqlite3"
|
|
18
|
-
SCHEMA_VERSION =
|
|
18
|
+
SCHEMA_VERSION = 2
|
|
19
19
|
|
|
20
20
|
class << self
|
|
21
21
|
attr_accessor :db
|
|
@@ -82,7 +82,10 @@ module Git
|
|
|
82
82
|
Git::Pkgs::Models::Commit,
|
|
83
83
|
Git::Pkgs::Models::Manifest,
|
|
84
84
|
Git::Pkgs::Models::DependencyChange,
|
|
85
|
-
Git::Pkgs::Models::DependencySnapshot
|
|
85
|
+
Git::Pkgs::Models::DependencySnapshot,
|
|
86
|
+
Git::Pkgs::Models::Package,
|
|
87
|
+
Git::Pkgs::Models::Vulnerability,
|
|
88
|
+
Git::Pkgs::Models::VulnerabilityPackage
|
|
86
89
|
].each do |model|
|
|
87
90
|
model.dataset = @db[model.table_name]
|
|
88
91
|
# Clear all cached association data that may reference old db
|
|
@@ -157,6 +160,7 @@ module Git
|
|
|
157
160
|
foreign_key :manifest_id, :manifests
|
|
158
161
|
String :name, null: false
|
|
159
162
|
String :ecosystem
|
|
163
|
+
String :purl
|
|
160
164
|
String :change_type, null: false
|
|
161
165
|
String :requirement
|
|
162
166
|
String :previous_requirement
|
|
@@ -171,12 +175,63 @@ module Git
|
|
|
171
175
|
foreign_key :manifest_id, :manifests
|
|
172
176
|
String :name, null: false
|
|
173
177
|
String :ecosystem
|
|
178
|
+
String :purl
|
|
174
179
|
String :requirement
|
|
175
180
|
String :dependency_type
|
|
176
181
|
DateTime :created_at
|
|
177
182
|
DateTime :updated_at
|
|
178
183
|
end
|
|
179
184
|
|
|
185
|
+
@db.create_table?(:packages) do
|
|
186
|
+
primary_key :id
|
|
187
|
+
String :purl, null: false
|
|
188
|
+
String :ecosystem, null: false
|
|
189
|
+
String :name, null: false
|
|
190
|
+
String :latest_version
|
|
191
|
+
String :license
|
|
192
|
+
String :description, text: true
|
|
193
|
+
String :homepage
|
|
194
|
+
String :repository_url
|
|
195
|
+
String :source
|
|
196
|
+
DateTime :enriched_at
|
|
197
|
+
DateTime :vulns_synced_at
|
|
198
|
+
DateTime :created_at
|
|
199
|
+
DateTime :updated_at
|
|
200
|
+
index :purl, unique: true
|
|
201
|
+
index [:ecosystem, :name]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Core vulnerability data (one row per CVE/GHSA)
|
|
205
|
+
@db.create_table?(:vulnerabilities) do
|
|
206
|
+
String :id, primary_key: true # CVE-2024-1234, GHSA-xxxx, etc.
|
|
207
|
+
String :aliases, text: true # comma-separated other IDs for same vuln
|
|
208
|
+
String :severity # critical, high, medium, low
|
|
209
|
+
Float :cvss_score
|
|
210
|
+
String :cvss_vector
|
|
211
|
+
String :references, text: true # JSON array of {type, url} objects
|
|
212
|
+
String :summary, text: true
|
|
213
|
+
String :details, text: true
|
|
214
|
+
DateTime :published_at # when vuln was disclosed
|
|
215
|
+
DateTime :withdrawn_at # when vuln was retracted (if ever)
|
|
216
|
+
DateTime :modified_at # when OSV record was last modified
|
|
217
|
+
DateTime :fetched_at, null: false # when we last fetched from OSV
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Which packages are affected by each vulnerability
|
|
221
|
+
# One vuln can affect multiple packages, each with different version ranges
|
|
222
|
+
@db.create_table?(:vulnerability_packages) do
|
|
223
|
+
primary_key :id
|
|
224
|
+
String :vulnerability_id, null: false
|
|
225
|
+
String :ecosystem, null: false # OSV ecosystem name
|
|
226
|
+
String :package_name, null: false
|
|
227
|
+
String :affected_versions, text: true # version range expression
|
|
228
|
+
String :fixed_versions, text: true # comma-separated list
|
|
229
|
+
foreign_key [:vulnerability_id], :vulnerabilities
|
|
230
|
+
index [:ecosystem, :package_name]
|
|
231
|
+
index [:vulnerability_id]
|
|
232
|
+
unique [:vulnerability_id, :ecosystem, :package_name]
|
|
233
|
+
end
|
|
234
|
+
|
|
180
235
|
set_version
|
|
181
236
|
create_bulk_indexes if with_indexes
|
|
182
237
|
refresh_models
|
|
@@ -186,6 +241,7 @@ module Git
|
|
|
186
241
|
@db.alter_table(:dependency_changes) do
|
|
187
242
|
add_index :name, if_not_exists: true
|
|
188
243
|
add_index :ecosystem, if_not_exists: true
|
|
244
|
+
add_index :purl, if_not_exists: true
|
|
189
245
|
add_index [:commit_id, :name], if_not_exists: true
|
|
190
246
|
end
|
|
191
247
|
|
|
@@ -193,6 +249,7 @@ module Git
|
|
|
193
249
|
add_index [:commit_id, :manifest_id, :name], unique: true, name: "idx_snapshots_unique", if_not_exists: true
|
|
194
250
|
add_index :name, if_not_exists: true
|
|
195
251
|
add_index :ecosystem, if_not_exists: true
|
|
252
|
+
add_index :purl, if_not_exists: true
|
|
196
253
|
end
|
|
197
254
|
end
|
|
198
255
|
|
|
@@ -218,10 +275,83 @@ module Git
|
|
|
218
275
|
def self.check_version!
|
|
219
276
|
return unless needs_upgrade?
|
|
220
277
|
|
|
278
|
+
migrate!
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def self.migrate!
|
|
221
282
|
stored = stored_version || 0
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
283
|
+
|
|
284
|
+
# Migration from v1 to v2: add vuln tables
|
|
285
|
+
if stored < 2
|
|
286
|
+
migrate_to_v2!
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
set_version
|
|
290
|
+
refresh_models
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def self.migrate_to_v2!
|
|
294
|
+
@db.create_table?(:packages) do
|
|
295
|
+
primary_key :id
|
|
296
|
+
String :purl, null: false
|
|
297
|
+
String :ecosystem, null: false
|
|
298
|
+
String :name, null: false
|
|
299
|
+
String :latest_version
|
|
300
|
+
String :license
|
|
301
|
+
String :description, text: true
|
|
302
|
+
String :homepage
|
|
303
|
+
String :repository_url
|
|
304
|
+
String :source
|
|
305
|
+
DateTime :enriched_at
|
|
306
|
+
DateTime :vulns_synced_at
|
|
307
|
+
DateTime :created_at
|
|
308
|
+
DateTime :updated_at
|
|
309
|
+
index :purl, unique: true
|
|
310
|
+
index [:ecosystem, :name]
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
@db.create_table?(:vulnerabilities) do
|
|
314
|
+
String :id, primary_key: true
|
|
315
|
+
String :aliases, text: true
|
|
316
|
+
String :severity
|
|
317
|
+
Float :cvss_score
|
|
318
|
+
String :cvss_vector
|
|
319
|
+
String :references, text: true
|
|
320
|
+
String :summary, text: true
|
|
321
|
+
String :details, text: true
|
|
322
|
+
DateTime :published_at
|
|
323
|
+
DateTime :withdrawn_at
|
|
324
|
+
DateTime :modified_at
|
|
325
|
+
DateTime :fetched_at, null: false
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
@db.create_table?(:vulnerability_packages) do
|
|
329
|
+
primary_key :id
|
|
330
|
+
String :vulnerability_id, null: false
|
|
331
|
+
String :ecosystem, null: false
|
|
332
|
+
String :package_name, null: false
|
|
333
|
+
String :affected_versions, text: true
|
|
334
|
+
String :fixed_versions, text: true
|
|
335
|
+
foreign_key [:vulnerability_id], :vulnerabilities
|
|
336
|
+
index [:ecosystem, :package_name]
|
|
337
|
+
index [:vulnerability_id]
|
|
338
|
+
unique [:vulnerability_id, :ecosystem, :package_name]
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Add purl column to existing tables if missing
|
|
342
|
+
unless @db.schema(:dependency_changes).any? { |col, _| col == :purl }
|
|
343
|
+
@db.alter_table(:dependency_changes) do
|
|
344
|
+
add_column :purl, String
|
|
345
|
+
add_index :purl, if_not_exists: true
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
unless @db.schema(:dependency_snapshots).any? { |col, _| col == :purl }
|
|
350
|
+
@db.alter_table(:dependency_snapshots) do
|
|
351
|
+
add_column :purl, String
|
|
352
|
+
add_index :purl, if_not_exists: true
|
|
353
|
+
end
|
|
354
|
+
end
|
|
225
355
|
end
|
|
226
356
|
|
|
227
357
|
def self.optimize_for_bulk_writes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
# Maps ecosystem names between bibliothecary, purl, and OSV formats.
|
|
6
|
+
# Bibliothecary uses lowercase names internally.
|
|
7
|
+
# Purl uses its own type names.
|
|
8
|
+
# OSV uses mixed case names that differ from both.
|
|
9
|
+
module Ecosystems
|
|
10
|
+
# Mapping from bibliothecary ecosystem names to OSV and purl equivalents
|
|
11
|
+
MAPPINGS = {
|
|
12
|
+
"npm" => { osv: "npm", purl: "npm" },
|
|
13
|
+
"rubygems" => { osv: "RubyGems", purl: "gem" },
|
|
14
|
+
"pypi" => { osv: "PyPI", purl: "pypi" },
|
|
15
|
+
"cargo" => { osv: "crates.io", purl: "cargo" },
|
|
16
|
+
"maven" => { osv: "Maven", purl: "maven" },
|
|
17
|
+
"nuget" => { osv: "NuGet", purl: "nuget" },
|
|
18
|
+
"packagist" => { osv: "Packagist", purl: "composer" },
|
|
19
|
+
"go" => { osv: "Go", purl: "golang" },
|
|
20
|
+
"hex" => { osv: "Hex", purl: "hex" },
|
|
21
|
+
"pub" => { osv: "Pub", purl: "pub" }
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# Reverse mappings for lookups from OSV/purl to bibliothecary
|
|
25
|
+
OSV_TO_BIBLIOTHECARY = MAPPINGS.transform_values { |v| v[:osv] }.invert.freeze
|
|
26
|
+
PURL_TO_BIBLIOTHECARY = MAPPINGS.transform_values { |v| v[:purl] }.invert.freeze
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Convert bibliothecary ecosystem name to OSV format
|
|
30
|
+
# @param ecosystem [String] bibliothecary ecosystem name (e.g., "rubygems")
|
|
31
|
+
# @return [String, nil] OSV ecosystem name (e.g., "RubyGems") or nil if not mapped
|
|
32
|
+
def to_osv(ecosystem)
|
|
33
|
+
MAPPINGS.dig(ecosystem.to_s.downcase, :osv)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Convert bibliothecary ecosystem name to purl type
|
|
37
|
+
# @param ecosystem [String] bibliothecary ecosystem name (e.g., "rubygems")
|
|
38
|
+
# @return [String, nil] purl type (e.g., "gem") or nil if not mapped
|
|
39
|
+
def to_purl(ecosystem)
|
|
40
|
+
MAPPINGS.dig(ecosystem.to_s.downcase, :purl)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Convert OSV ecosystem name to bibliothecary format
|
|
44
|
+
# @param osv_ecosystem [String] OSV ecosystem name (e.g., "RubyGems")
|
|
45
|
+
# @return [String, nil] bibliothecary ecosystem name (e.g., "rubygems") or nil if not mapped
|
|
46
|
+
def from_osv(osv_ecosystem)
|
|
47
|
+
OSV_TO_BIBLIOTHECARY[osv_ecosystem]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Convert purl type to bibliothecary ecosystem name
|
|
51
|
+
# @param purl_type [String] purl type (e.g., "gem")
|
|
52
|
+
# @return [String, nil] bibliothecary ecosystem name (e.g., "rubygems") or nil if not mapped
|
|
53
|
+
def from_purl(purl_type)
|
|
54
|
+
PURL_TO_BIBLIOTHECARY[purl_type]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if an ecosystem is supported for vulnerability scanning
|
|
58
|
+
# @param ecosystem [String] bibliothecary ecosystem name
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def supported?(ecosystem)
|
|
61
|
+
MAPPINGS.key?(ecosystem.to_s.downcase)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# List all supported bibliothecary ecosystem names
|
|
65
|
+
# @return [Array<String>]
|
|
66
|
+
def supported_ecosystems
|
|
67
|
+
MAPPINGS.keys
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Generate a purl (package URL) for a given ecosystem and package name
|
|
71
|
+
# @param ecosystem [String] bibliothecary ecosystem name (e.g., "rubygems")
|
|
72
|
+
# @param name [String] package name
|
|
73
|
+
# @return [String, nil] purl string (e.g., "pkg:gem/rails") or nil if ecosystem not supported
|
|
74
|
+
def generate_purl(ecosystem, name)
|
|
75
|
+
purl_type = to_purl(ecosystem)
|
|
76
|
+
return nil unless purl_type
|
|
77
|
+
|
|
78
|
+
"pkg:#{purl_type}/#{name}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Models
|
|
6
|
+
class Package < Sequel::Model
|
|
7
|
+
STALE_THRESHOLD = 86400 # 24 hours
|
|
8
|
+
|
|
9
|
+
dataset_module do
|
|
10
|
+
def by_ecosystem(ecosystem)
|
|
11
|
+
where(ecosystem: ecosystem)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def needs_vuln_sync
|
|
15
|
+
where(vulns_synced_at: nil).or { vulns_synced_at < Time.now - STALE_THRESHOLD }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def synced
|
|
19
|
+
where { vulns_synced_at >= Time.now - STALE_THRESHOLD }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def needs_vuln_sync?
|
|
24
|
+
vulns_synced_at.nil? || vulns_synced_at < Time.now - STALE_THRESHOLD
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def mark_vulns_synced
|
|
28
|
+
update(vulns_synced_at: Time.now)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def vulnerabilities
|
|
32
|
+
osv_ecosystem = Ecosystems.to_osv(ecosystem)
|
|
33
|
+
return [] unless osv_ecosystem
|
|
34
|
+
|
|
35
|
+
VulnerabilityPackage
|
|
36
|
+
.where(ecosystem: osv_ecosystem, package_name: name)
|
|
37
|
+
.map(&:vulnerability)
|
|
38
|
+
.compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.find_or_create_by_purl(purl:, ecosystem: nil, name: nil)
|
|
42
|
+
existing = first(purl: purl)
|
|
43
|
+
return existing if existing
|
|
44
|
+
|
|
45
|
+
create(purl: purl, ecosystem: ecosystem, name: name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.generate_purl(ecosystem, name)
|
|
49
|
+
Ecosystems.generate_purl(ecosystem, name)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|