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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +28 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +15 -0
  5. data/Dockerfile +18 -0
  6. data/Formula/git-pkgs.rb +28 -0
  7. data/README.md +36 -4
  8. data/lib/git/pkgs/analyzer.rb +141 -9
  9. data/lib/git/pkgs/cli.rb +16 -6
  10. data/lib/git/pkgs/commands/blame.rb +0 -18
  11. data/lib/git/pkgs/commands/diff.rb +122 -5
  12. data/lib/git/pkgs/commands/diff_driver.rb +24 -4
  13. data/lib/git/pkgs/commands/init.rb +5 -0
  14. data/lib/git/pkgs/commands/list.rb +60 -15
  15. data/lib/git/pkgs/commands/show.rb +126 -3
  16. data/lib/git/pkgs/commands/stale.rb +6 -2
  17. data/lib/git/pkgs/commands/update.rb +3 -0
  18. data/lib/git/pkgs/commands/vulns/base.rb +354 -0
  19. data/lib/git/pkgs/commands/vulns/blame.rb +276 -0
  20. data/lib/git/pkgs/commands/vulns/diff.rb +172 -0
  21. data/lib/git/pkgs/commands/vulns/exposure.rb +418 -0
  22. data/lib/git/pkgs/commands/vulns/history.rb +345 -0
  23. data/lib/git/pkgs/commands/vulns/log.rb +218 -0
  24. data/lib/git/pkgs/commands/vulns/praise.rb +238 -0
  25. data/lib/git/pkgs/commands/vulns/scan.rb +231 -0
  26. data/lib/git/pkgs/commands/vulns/show.rb +216 -0
  27. data/lib/git/pkgs/commands/vulns/sync.rb +108 -0
  28. data/lib/git/pkgs/commands/vulns.rb +50 -0
  29. data/lib/git/pkgs/config.rb +8 -1
  30. data/lib/git/pkgs/database.rb +135 -5
  31. data/lib/git/pkgs/ecosystems.rb +83 -0
  32. data/lib/git/pkgs/models/package.rb +54 -0
  33. data/lib/git/pkgs/models/vulnerability.rb +300 -0
  34. data/lib/git/pkgs/models/vulnerability_package.rb +59 -0
  35. data/lib/git/pkgs/osv_client.rb +151 -0
  36. data/lib/git/pkgs/output.rb +22 -0
  37. data/lib/git/pkgs/version.rb +1 -1
  38. data/lib/git/pkgs.rb +6 -0
  39. metadata +66 -4
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Git
6
+ module Pkgs
7
+ module Models
8
+ class Vulnerability < Sequel::Model
9
+ one_to_many :vulnerability_packages, key: :vulnerability_id
10
+
11
+ dataset_module do
12
+ def by_severity(severity)
13
+ where(severity: severity)
14
+ end
15
+
16
+ def critical
17
+ by_severity("critical")
18
+ end
19
+
20
+ def high
21
+ by_severity("high")
22
+ end
23
+
24
+ def medium
25
+ by_severity("medium")
26
+ end
27
+
28
+ def low
29
+ by_severity("low")
30
+ end
31
+
32
+ def not_withdrawn
33
+ where(withdrawn_at: nil)
34
+ end
35
+
36
+ def stale(max_age_seconds = 86400)
37
+ threshold = Time.now - max_age_seconds
38
+ where { fetched_at < threshold }
39
+ end
40
+
41
+ def fresh(max_age_seconds = 86400)
42
+ threshold = Time.now - max_age_seconds
43
+ where { fetched_at >= threshold }
44
+ end
45
+ end
46
+
47
+ def severity_level
48
+ case severity&.downcase
49
+ when "critical" then 4
50
+ when "high" then 3
51
+ when "medium" then 2
52
+ when "low" then 1
53
+ else 0
54
+ end
55
+ end
56
+
57
+ def severity_display
58
+ severity&.upcase || "UNKNOWN"
59
+ end
60
+
61
+ def withdrawn?
62
+ !withdrawn_at.nil?
63
+ end
64
+
65
+ def aliases_list
66
+ return [] if aliases.nil? || aliases.empty?
67
+
68
+ aliases.split(",").map(&:strip)
69
+ end
70
+
71
+ # Create or update from OSV API response data.
72
+ # Creates both the Vulnerability record and VulnerabilityPackage records
73
+ # for each affected package.
74
+ def self.from_osv(osv_data)
75
+ vuln_id = osv_data["id"]
76
+ severity_info = extract_severity(osv_data)
77
+
78
+ vuln = update_or_create(
79
+ { id: vuln_id },
80
+ {
81
+ aliases: extract_aliases(osv_data),
82
+ severity: severity_info[:severity],
83
+ cvss_score: severity_info[:score],
84
+ cvss_vector: severity_info[:vector],
85
+ summary: osv_data["summary"],
86
+ details: osv_data["details"],
87
+ published_at: parse_timestamp(osv_data["published"]),
88
+ modified_at: parse_timestamp(osv_data["modified"]),
89
+ withdrawn_at: parse_timestamp(osv_data["withdrawn"]),
90
+ fetched_at: Time.now
91
+ }
92
+ )
93
+
94
+ # Create VulnerabilityPackage records for each affected package
95
+ (osv_data["affected"] || []).each do |affected|
96
+ pkg = affected["package"]
97
+ next unless pkg
98
+
99
+ ecosystem = pkg["ecosystem"]
100
+ name = pkg["name"]
101
+
102
+ affected_range = build_affected_range(affected)
103
+ fixed = extract_fixed_versions(affected)
104
+
105
+ VulnerabilityPackage.update_or_create(
106
+ { vulnerability_id: vuln_id, ecosystem: ecosystem, package_name: name },
107
+ {
108
+ affected_versions: affected_range,
109
+ fixed_versions: fixed&.join(",")
110
+ }
111
+ )
112
+ end
113
+
114
+ vuln
115
+ end
116
+
117
+ def self.extract_aliases(osv_data)
118
+ aliases = osv_data["aliases"] || []
119
+ aliases.any? ? aliases.join(",") : nil
120
+ end
121
+
122
+ def self.extract_severity(osv_data)
123
+ result = { severity: nil, score: nil, vector: nil }
124
+
125
+ if osv_data["severity"]&.any?
126
+ sev = osv_data["severity"].first
127
+ result[:vector] = sev["score"]
128
+
129
+ if sev["score"]&.include?("CVSS")
130
+ result[:score] = parse_cvss_score(sev["score"])
131
+ result[:severity] = score_to_severity(result[:score])
132
+ end
133
+ end
134
+
135
+ # Check root-level database_specific (GHSA format)
136
+ if osv_data["database_specific"]&.dig("severity")
137
+ result[:severity] ||= normalize_severity(osv_data["database_specific"]["severity"])
138
+ end
139
+
140
+ # Check affected entries for database_specific severity
141
+ osv_data["affected"]&.each do |affected|
142
+ db_specific = affected["database_specific"]
143
+ if db_specific && db_specific["severity"]
144
+ result[:severity] ||= normalize_severity(db_specific["severity"])
145
+ end
146
+ end
147
+
148
+ result
149
+ end
150
+
151
+ def self.normalize_severity(severity)
152
+ return nil unless severity
153
+
154
+ case severity.downcase
155
+ when "critical" then "critical"
156
+ when "high" then "high"
157
+ when "moderate", "medium" then "medium"
158
+ when "low" then "low"
159
+ end
160
+ end
161
+
162
+ def self.parse_cvss_score(vector)
163
+ return nil unless vector
164
+
165
+ if vector.match?(/\A\d+(\.\d+)?\z/)
166
+ return vector.to_f
167
+ end
168
+
169
+ return nil unless vector.include?("CVSS:")
170
+
171
+ metrics = parse_cvss_metrics(vector)
172
+ return nil if metrics.empty?
173
+
174
+ estimate_cvss_score(metrics)
175
+ end
176
+
177
+ def self.parse_cvss_metrics(vector)
178
+ metrics = {}
179
+ vector.split("/").each do |part|
180
+ key, value = part.split(":")
181
+ metrics[key] = value if key && value
182
+ end
183
+ metrics
184
+ end
185
+
186
+ def self.estimate_cvss_score(metrics)
187
+ impact_values = { "N" => 0, "L" => 1, "H" => 2 }
188
+ c = impact_values[metrics["C"]] || 0
189
+ i = impact_values[metrics["I"]] || 0
190
+ a = impact_values[metrics["A"]] || 0
191
+ max_impact = [c, i, a].max
192
+
193
+ ac_easy = metrics["AC"] == "L"
194
+ av_network = metrics["AV"] == "N"
195
+ pr_none = metrics["PR"] == "N"
196
+ ui_none = metrics["UI"] == "N"
197
+
198
+ if max_impact == 2 && av_network && ac_easy && pr_none && ui_none
199
+ 9.8
200
+ elsif max_impact == 2 && av_network && ac_easy
201
+ 8.1
202
+ elsif max_impact == 2
203
+ 7.0
204
+ elsif max_impact == 1 && av_network
205
+ 5.3
206
+ elsif max_impact == 1
207
+ 4.0
208
+ elsif max_impact == 0
209
+ 0.0
210
+ else
211
+ 5.0
212
+ end
213
+ end
214
+
215
+ def self.score_to_severity(score)
216
+ return nil unless score
217
+
218
+ case score
219
+ when 9.0..10.0 then "critical"
220
+ when 7.0...9.0 then "high"
221
+ when 4.0...7.0 then "medium"
222
+ when 0.0...4.0 then "low"
223
+ end
224
+ end
225
+
226
+ def self.build_affected_range(affected)
227
+ return nil unless affected
228
+
229
+ ranges = affected["ranges"] || []
230
+ versions = affected["versions"] || []
231
+
232
+ return versions.join(",") if versions.any? && ranges.empty?
233
+
234
+ range_parts = ranges.flat_map do |range|
235
+ events = range["events"] || []
236
+ build_range_from_events(events)
237
+ end
238
+
239
+ range_parts.compact.join(" || ")
240
+ end
241
+
242
+ def self.build_range_from_events(events)
243
+ ranges = []
244
+ current_introduced = nil
245
+
246
+ events.each do |event|
247
+ if event["introduced"]
248
+ current_introduced = event["introduced"]
249
+ elsif event["fixed"] && current_introduced
250
+ if current_introduced == "0"
251
+ ranges << "<#{event["fixed"]}"
252
+ else
253
+ ranges << ">=#{current_introduced} <#{event["fixed"]}"
254
+ end
255
+ current_introduced = nil
256
+ elsif event["last_affected"] && current_introduced
257
+ if current_introduced == "0"
258
+ ranges << "<=#{event["last_affected"]}"
259
+ else
260
+ ranges << ">=#{current_introduced} <=#{event["last_affected"]}"
261
+ end
262
+ current_introduced = nil
263
+ end
264
+ end
265
+
266
+ if current_introduced
267
+ ranges << if current_introduced == "0"
268
+ ">=0"
269
+ else
270
+ ">=#{current_introduced}"
271
+ end
272
+ end
273
+
274
+ ranges
275
+ end
276
+
277
+ def self.extract_fixed_versions(affected)
278
+ return nil unless affected
279
+
280
+ fixed = []
281
+ (affected["ranges"] || []).each do |range|
282
+ (range["events"] || []).each do |event|
283
+ fixed << event["fixed"] if event["fixed"]
284
+ end
285
+ end
286
+
287
+ fixed.uniq.empty? ? nil : fixed.uniq
288
+ end
289
+
290
+ def self.parse_timestamp(str)
291
+ return nil unless str
292
+
293
+ Time.parse(str)
294
+ rescue ArgumentError
295
+ nil
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "vers"
4
+
5
+ module Git
6
+ module Pkgs
7
+ module Models
8
+ class VulnerabilityPackage < Sequel::Model
9
+ many_to_one :vulnerability, key: :vulnerability_id
10
+
11
+ dataset_module do
12
+ def for_package(ecosystem, name)
13
+ where(ecosystem: ecosystem, package_name: name)
14
+ end
15
+ end
16
+
17
+ def affects_version?(version)
18
+ return false if affected_versions.nil? || affected_versions.empty?
19
+ return false if version.nil? || version.empty?
20
+
21
+ # Convert OSV ecosystem to purl type for Vers
22
+ bib_ecosystem = Ecosystems.from_osv(ecosystem) || ecosystem.downcase
23
+ purl_type = Ecosystems.to_purl(bib_ecosystem) || bib_ecosystem
24
+
25
+ # Handle || separator (OR conditions between different ranges)
26
+ # Each part separated by || is an independent range (OR)
27
+ # Within each part, space-separated constraints are AND conditions
28
+ affected_versions.split(" || ").any? do |range_part|
29
+ range_matches?(version, range_part, purl_type)
30
+ end
31
+ rescue ArgumentError, Vers::Error
32
+ # If we can't parse the version or range, be conservative and assume affected
33
+ true
34
+ end
35
+
36
+ def range_matches?(version, range_part, purl_type)
37
+ # Extract individual constraints (e.g., ">=7.1.0 <7.1.3.1" -> [">=7.1.0", "<7.1.3.1"])
38
+ constraints = range_part.scan(/[<>=!~^]+[^\s]+/)
39
+ return false if constraints.empty?
40
+
41
+ # All constraints must be satisfied (AND logic)
42
+ constraints.all? do |constraint|
43
+ Vers.satisfies?(version, constraint, purl_type)
44
+ end
45
+ end
46
+
47
+ def fixed_versions_list
48
+ return [] if fixed_versions.nil? || fixed_versions.empty?
49
+
50
+ fixed_versions.split(",").map(&:strip)
51
+ end
52
+
53
+ def purl
54
+ Models::Package.generate_purl(ecosystem, package_name)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Git
8
+ module Pkgs
9
+ # Client for the OSV (Open Source Vulnerabilities) API.
10
+ # https://google.github.io/osv.dev/api/
11
+ class OsvClient
12
+ API_BASE = "https://api.osv.dev/v1"
13
+ BATCH_SIZE = 1000 # Max queries per batch request
14
+
15
+ class Error < StandardError; end
16
+ class ApiError < Error; end
17
+
18
+ def initialize
19
+ @http_clients = {}
20
+ end
21
+
22
+ # Query vulnerabilities for a single package version.
23
+ #
24
+ # @param ecosystem [String] OSV ecosystem name (e.g., "RubyGems")
25
+ # @param name [String] package name
26
+ # @param version [String] package version
27
+ # @return [Array<Hash>] array of vulnerability hashes
28
+ def query(ecosystem:, name:, version:)
29
+ payload = {
30
+ package: {
31
+ name: name,
32
+ ecosystem: ecosystem
33
+ },
34
+ version: version
35
+ }
36
+
37
+ response = post("/query", payload)
38
+ fetch_all_pages(response, payload)
39
+ end
40
+
41
+ # Batch query vulnerabilities for multiple packages.
42
+ # More efficient than individual queries for large dependency sets.
43
+ #
44
+ # @param packages [Array<Hash>] array of {ecosystem:, name:, version:} hashes
45
+ # @return [Array<Array<Hash>>] array of vulnerability arrays, one per input package
46
+ def query_batch(packages)
47
+ return [] if packages.empty?
48
+
49
+ results = Array.new(packages.size) { [] }
50
+
51
+ packages.each_slice(BATCH_SIZE).with_index do |batch, batch_idx|
52
+ queries = batch.map do |pkg|
53
+ {
54
+ package: {
55
+ name: pkg[:name],
56
+ ecosystem: pkg[:ecosystem]
57
+ },
58
+ version: pkg[:version]
59
+ }
60
+ end
61
+
62
+ response = post("/querybatch", { queries: queries })
63
+ batch_results = response["results"] || []
64
+
65
+ batch_results.each_with_index do |result, idx|
66
+ global_idx = batch_idx * BATCH_SIZE + idx
67
+ results[global_idx] = result["vulns"] || []
68
+ end
69
+ end
70
+
71
+ results
72
+ end
73
+
74
+ # Fetch full details for a specific vulnerability by ID.
75
+ #
76
+ # @param vuln_id [String] vulnerability ID (e.g., "CVE-2024-1234", "GHSA-xxxx")
77
+ # @return [Hash] full vulnerability data
78
+ def get_vulnerability(vuln_id)
79
+ get("/vulns/#{URI.encode_uri_component(vuln_id)}")
80
+ end
81
+
82
+ private
83
+
84
+ def post(path, payload)
85
+ uri = URI("#{API_BASE}#{path}")
86
+ request = Net::HTTP::Post.new(uri)
87
+ request["Content-Type"] = "application/json"
88
+ request.body = JSON.generate(payload)
89
+
90
+ execute_request(uri, request)
91
+ end
92
+
93
+ def get(path)
94
+ uri = URI("#{API_BASE}#{path}")
95
+ request = Net::HTTP::Get.new(uri)
96
+ request["Content-Type"] = "application/json"
97
+
98
+ execute_request(uri, request)
99
+ end
100
+
101
+ def execute_request(uri, request)
102
+ http = http_client(uri)
103
+ response = http.request(request)
104
+
105
+ case response
106
+ when Net::HTTPSuccess
107
+ JSON.parse(response.body)
108
+ else
109
+ raise ApiError, "OSV API error: #{response.code} #{response.message}"
110
+ end
111
+ rescue JSON::ParserError => e
112
+ raise ApiError, "Invalid JSON response from OSV API: #{e.message}"
113
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
114
+ raise ApiError, "OSV API timeout: #{e.message}"
115
+ rescue SocketError, Errno::ECONNREFUSED => e
116
+ raise ApiError, "OSV API connection error: #{e.message}"
117
+ rescue OpenSSL::SSL::SSLError => e
118
+ raise ApiError, "OSV API SSL error: #{e.message}"
119
+ end
120
+
121
+ def http_client(uri)
122
+ key = "#{uri.host}:#{uri.port}"
123
+ @http_clients[key] ||= begin
124
+ http = Net::HTTP.new(uri.host, uri.port)
125
+ http.use_ssl = uri.scheme == "https"
126
+ http.open_timeout = 10
127
+ http.read_timeout = 30
128
+ http
129
+ end
130
+ end
131
+
132
+ MAX_PAGES = 100
133
+
134
+ def fetch_all_pages(response, original_payload)
135
+ vulns = response["vulns"] || []
136
+ page_token = response["next_page_token"]
137
+ pages_fetched = 0
138
+
139
+ while page_token && pages_fetched < MAX_PAGES
140
+ payload = original_payload.merge(page_token: page_token)
141
+ response = post("/query", payload)
142
+ vulns.concat(response["vulns"] || [])
143
+ page_token = response["next_page_token"]
144
+ pages_fetched += 1
145
+ end
146
+
147
+ vulns
148
+ end
149
+ end
150
+ end
151
+ end
@@ -52,6 +52,28 @@ module Git
52
52
 
53
53
  error "Database not initialized. Run 'git pkgs init' first."
54
54
  end
55
+
56
+ # Pick best author from commit, preferring humans over bots
57
+ def best_author(commit)
58
+ author_name = commit.respond_to?(:author_name) ? commit.author_name : commit[:author_name]
59
+ message = commit.respond_to?(:message) ? commit.message : commit[:message]
60
+
61
+ authors = [author_name] + parse_coauthors(message)
62
+
63
+ # Prefer human authors over bots
64
+ human = authors.find { |a| !bot_author?(a) }
65
+ human || authors.first
66
+ end
67
+
68
+ def parse_coauthors(message)
69
+ return [] unless message
70
+
71
+ message.scan(/^Co-authored-by:([^<]+)<[^>]+>/i).flatten.map(&:strip)
72
+ end
73
+
74
+ def bot_author?(name)
75
+ name =~ /\[bot\]$|^dependabot|^renovate|^github-actions/i
76
+ end
55
77
  end
56
78
  end
57
79
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Git
4
4
  module Pkgs
5
- VERSION = "0.6.2"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
data/lib/git/pkgs.rb CHANGED
@@ -8,6 +8,8 @@ require_relative "pkgs/cli"
8
8
  require_relative "pkgs/database"
9
9
  require_relative "pkgs/repository"
10
10
  require_relative "pkgs/analyzer"
11
+ require_relative "pkgs/ecosystems"
12
+ require_relative "pkgs/osv_client"
11
13
 
12
14
  require_relative "pkgs/models/branch"
13
15
  require_relative "pkgs/models/branch_commit"
@@ -15,6 +17,9 @@ require_relative "pkgs/models/commit"
15
17
  require_relative "pkgs/models/manifest"
16
18
  require_relative "pkgs/models/dependency_change"
17
19
  require_relative "pkgs/models/dependency_snapshot"
20
+ require_relative "pkgs/models/package"
21
+ require_relative "pkgs/models/vulnerability"
22
+ require_relative "pkgs/models/vulnerability_package"
18
23
 
19
24
  require_relative "pkgs/commands/init"
20
25
  require_relative "pkgs/commands/update"
@@ -37,6 +42,7 @@ require_relative "pkgs/commands/upgrade"
37
42
  require_relative "pkgs/commands/schema"
38
43
  require_relative "pkgs/commands/diff_driver"
39
44
  require_relative "pkgs/commands/completions"
45
+ require_relative "pkgs/commands/vulns"
40
46
 
41
47
  module Git
42
48
  module Pkgs
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-pkgs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
@@ -57,14 +57,56 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '15.1'
60
+ version: '15.2'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '15.1'
67
+ version: '15.2'
68
+ - !ruby/object:Gem::Dependency
69
+ name: vers
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: purl
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.7'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.7'
96
+ - !ruby/object:Gem::Dependency
97
+ name: sarif-ruby
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
68
110
  description: A git subcommand for analyzing package/dependency usage in git repositories
69
111
  over time
70
112
  email:
@@ -74,7 +116,11 @@ executables:
74
116
  extensions: []
75
117
  extra_rdoc_files: []
76
118
  files:
119
+ - ".gitattributes"
120
+ - ".ruby-version"
77
121
  - CHANGELOG.md
122
+ - Dockerfile
123
+ - Formula/git-pkgs.rb
78
124
  - LICENSE
79
125
  - README.md
80
126
  - exe/git-pkgs
@@ -101,16 +147,32 @@ files:
101
147
  - lib/git/pkgs/commands/tree.rb
102
148
  - lib/git/pkgs/commands/update.rb
103
149
  - lib/git/pkgs/commands/upgrade.rb
150
+ - lib/git/pkgs/commands/vulns.rb
151
+ - lib/git/pkgs/commands/vulns/base.rb
152
+ - lib/git/pkgs/commands/vulns/blame.rb
153
+ - lib/git/pkgs/commands/vulns/diff.rb
154
+ - lib/git/pkgs/commands/vulns/exposure.rb
155
+ - lib/git/pkgs/commands/vulns/history.rb
156
+ - lib/git/pkgs/commands/vulns/log.rb
157
+ - lib/git/pkgs/commands/vulns/praise.rb
158
+ - lib/git/pkgs/commands/vulns/scan.rb
159
+ - lib/git/pkgs/commands/vulns/show.rb
160
+ - lib/git/pkgs/commands/vulns/sync.rb
104
161
  - lib/git/pkgs/commands/where.rb
105
162
  - lib/git/pkgs/commands/why.rb
106
163
  - lib/git/pkgs/config.rb
107
164
  - lib/git/pkgs/database.rb
165
+ - lib/git/pkgs/ecosystems.rb
108
166
  - lib/git/pkgs/models/branch.rb
109
167
  - lib/git/pkgs/models/branch_commit.rb
110
168
  - lib/git/pkgs/models/commit.rb
111
169
  - lib/git/pkgs/models/dependency_change.rb
112
170
  - lib/git/pkgs/models/dependency_snapshot.rb
113
171
  - lib/git/pkgs/models/manifest.rb
172
+ - lib/git/pkgs/models/package.rb
173
+ - lib/git/pkgs/models/vulnerability.rb
174
+ - lib/git/pkgs/models/vulnerability_package.rb
175
+ - lib/git/pkgs/osv_client.rb
114
176
  - lib/git/pkgs/output.rb
115
177
  - lib/git/pkgs/pager.rb
116
178
  - lib/git/pkgs/repository.rb
@@ -137,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
199
  - !ruby/object:Gem::Version
138
200
  version: '0'
139
201
  requirements: []
140
- rubygems_version: 4.0.1
202
+ rubygems_version: 4.0.3
141
203
  specification_version: 4
142
204
  summary: Track package dependencies across git history
143
205
  test_files: []