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
@@ -80,10 +80,10 @@ module Git
80
80
 
81
81
  def install_driver
82
82
  # Set up git config for textconv
83
- system("git", "config", "diff.pkgs.textconv", "git-pkgs diff-driver")
83
+ git_config("diff.pkgs.textconv", "git-pkgs diff-driver")
84
84
 
85
85
  # Add to .gitattributes
86
- gitattributes_path = File.join(Dir.pwd, ".gitattributes")
86
+ gitattributes_path = File.join(work_tree, ".gitattributes")
87
87
  existing = File.exist?(gitattributes_path) ? File.read(gitattributes_path) : ""
88
88
 
89
89
  new_entries = []
@@ -109,9 +109,9 @@ module Git
109
109
  end
110
110
 
111
111
  def uninstall_driver
112
- system("git", "config", "--unset", "diff.pkgs.textconv")
112
+ git_config_unset("diff.pkgs.textconv")
113
113
 
114
- gitattributes_path = File.join(Dir.pwd, ".gitattributes")
114
+ gitattributes_path = File.join(work_tree, ".gitattributes")
115
115
  if File.exist?(gitattributes_path)
116
116
  lines = File.readlines(gitattributes_path)
117
117
  lines.reject! { |line| line.include?("diff=pkgs") || line.include?("# git-pkgs") }
@@ -140,6 +140,26 @@ module Git
140
140
  {}
141
141
  end
142
142
 
143
+ def work_tree
144
+ Git::Pkgs.work_tree || Dir.pwd
145
+ end
146
+
147
+ def git_cmd
148
+ if Git::Pkgs.git_dir
149
+ ["git", "-C", work_tree]
150
+ else
151
+ ["git"]
152
+ end
153
+ end
154
+
155
+ def git_config(key, value)
156
+ system(*git_cmd, "config", key, value)
157
+ end
158
+
159
+ def git_config_unset(key)
160
+ system(*git_cmd, "config", "--unset", key)
161
+ end
162
+
143
163
  def parse_options
144
164
  options = {}
145
165
 
@@ -102,6 +102,7 @@ module Git
102
102
  manifest_id: manifest_ids[c[:manifest_path]],
103
103
  name: c[:name],
104
104
  ecosystem: c[:ecosystem],
105
+ purl: c[:purl],
105
106
  change_type: c[:change_type],
106
107
  requirement: c[:requirement],
107
108
  previous_requirement: c[:previous_requirement],
@@ -121,6 +122,7 @@ module Git
121
122
  manifest_id: manifest_ids[s[:manifest_path]],
122
123
  name: s[:name],
123
124
  ecosystem: s[:ecosystem],
125
+ purl: s[:purl],
124
126
  requirement: s[:requirement],
125
127
  dependency_type: s[:dependency_type],
126
128
  created_at: now,
@@ -185,6 +187,7 @@ module Git
185
187
  manifest_path: manifest_key,
186
188
  name: change[:name],
187
189
  ecosystem: change[:ecosystem],
190
+ purl: change[:purl],
188
191
  change_type: change[:change_type],
189
192
  requirement: change[:requirement],
190
193
  previous_requirement: change[:previous_requirement],
@@ -202,6 +205,7 @@ module Git
202
205
  manifest_path: manifest_path,
203
206
  name: name,
204
207
  ecosystem: dep_info[:ecosystem],
208
+ purl: dep_info[:purl],
205
209
  requirement: dep_info[:requirement],
206
210
  dependency_type: dep_info[:dependency_type]
207
211
  }
@@ -222,6 +226,7 @@ module Git
222
226
  manifest_path: manifest_path,
223
227
  name: name,
224
228
  ecosystem: dep_info[:ecosystem],
229
+ purl: dep_info[:purl],
225
230
  requirement: dep_info[:requirement],
226
231
  dependency_type: dep_info[:dependency_type]
227
232
  }
@@ -13,16 +13,13 @@ module Git
13
13
 
14
14
  def run
15
15
  repo = Repository.new
16
- require_database(repo)
16
+ use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
17
17
 
18
- Database.connect(repo.git_dir)
19
-
20
- commit_sha = @options[:commit] || repo.head_sha
21
- target_commit = Models::Commit.first(sha: commit_sha)
22
-
23
- error "Commit #{commit_sha[0, 7]} not in database. Run 'git pkgs update' to index new commits." unless target_commit
24
-
25
- deps = compute_dependencies_at_commit(target_commit, repo)
18
+ if use_stateless
19
+ deps = run_stateless(repo)
20
+ else
21
+ deps = run_with_database(repo)
22
+ end
26
23
 
27
24
  # Apply filters
28
25
  if @options[:manifest]
@@ -42,22 +39,64 @@ module Git
42
39
  return
43
40
  end
44
41
 
42
+ locked_versions = build_locked_versions(deps)
43
+
45
44
  if @options[:format] == "json"
46
45
  require "json"
47
- puts JSON.pretty_generate(deps)
46
+ deps_with_locked = deps.map do |dep|
47
+ if dep[:kind] == "manifest"
48
+ locked = locked_versions[[dep[:ecosystem], dep[:name]]]
49
+ locked ? dep.merge(locked_version: locked) : dep
50
+ else
51
+ dep
52
+ end
53
+ end
54
+ puts JSON.pretty_generate(deps_with_locked)
48
55
  else
49
- paginate { output_text(deps) }
56
+ paginate { output_text(deps, locked_versions) }
50
57
  end
51
58
  end
52
59
 
53
- def output_text(deps)
60
+ def run_stateless(repo)
61
+ commit_sha = @options[:commit] || repo.head_sha
62
+ rugged_commit = repo.lookup(repo.rev_parse(commit_sha))
63
+
64
+ error "Could not resolve '#{commit_sha}'. Check that the ref exists." unless rugged_commit
65
+
66
+ analyzer = Analyzer.new(repo)
67
+ analyzer.dependencies_at_commit(rugged_commit)
68
+ end
69
+
70
+ def run_with_database(repo)
71
+ Database.connect(repo.git_dir)
72
+
73
+ commit_sha = @options[:commit] || repo.head_sha
74
+ target_commit = Models::Commit.first(sha: commit_sha)
75
+
76
+ error "Commit #{commit_sha[0, 7]} not in database. Run 'git pkgs update' to index new commits." unless target_commit
77
+
78
+ compute_dependencies_at_commit(target_commit, repo)
79
+ end
80
+
81
+ def build_locked_versions(deps)
82
+ locked_versions = {}
83
+ deps.each do |d|
84
+ next unless d[:kind] == "lockfile"
85
+ locked_versions[[d[:ecosystem], d[:name]]] = d[:requirement]
86
+ end
87
+ locked_versions
88
+ end
89
+
90
+ def output_text(deps, locked_versions)
54
91
  grouped = deps.group_by { |d| [d[:manifest_path], d[:ecosystem]] }
55
92
 
56
93
  grouped.each do |(path, platform), manifest_deps|
57
94
  puts "#{path} (#{platform}):"
58
95
  manifest_deps.sort_by { |d| d[:name] }.each do |dep|
59
- type_suffix = dep[:dependency_type] ? " [#{dep[:dependency_type]}]" : ""
60
- puts " #{dep[:name]} #{dep[:requirement]}#{type_suffix}"
96
+ type_suffix = dep[:dependency_type] && dep[:dependency_type] != "runtime" ? " [#{dep[:dependency_type]}]" : ""
97
+ locked = locked_versions[[dep[:ecosystem], dep[:name]]] if dep[:kind] == "manifest"
98
+ locked_suffix = locked ? " [#{locked}]" : ""
99
+ puts " #{dep[:name]} #{dep[:requirement]}#{locked_suffix}#{type_suffix}"
61
100
  end
62
101
  puts
63
102
  end
@@ -85,6 +124,7 @@ module Git
85
124
  manifest_path: s.manifest.path,
86
125
  name: s.name,
87
126
  ecosystem: s.ecosystem,
127
+ kind: s.manifest.kind,
88
128
  requirement: s.requirement,
89
129
  dependency_type: s.dependency_type
90
130
  }
@@ -93,7 +133,7 @@ module Git
93
133
 
94
134
  # Replay changes from snapshot to target
95
135
  if snapshot_commit && snapshot_commit.id != target_commit.id
96
- commit_ids = branch.commits_dataset.select_map(:id)
136
+ commit_ids = branch.commits_dataset.select_map(Sequel[:commits][:id])
97
137
  changes = Models::DependencyChange
98
138
  .join(:commits, id: :commit_id)
99
139
  .where(Sequel[:commits][:id] => commit_ids)
@@ -111,6 +151,7 @@ module Git
111
151
  manifest_path: change.manifest.path,
112
152
  name: change.name,
113
153
  ecosystem: change.ecosystem,
154
+ kind: change.manifest.kind,
114
155
  requirement: change.requirement,
115
156
  dependency_type: change.dependency_type
116
157
  }
@@ -157,6 +198,10 @@ module Git
157
198
  options[:no_pager] = true
158
199
  end
159
200
 
201
+ opts.on("--stateless", "Parse manifests directly without database") do
202
+ options[:stateless] = true
203
+ end
204
+
160
205
  opts.on("-h", "--help", "Show this help") do
161
206
  puts opts
162
207
  exit
@@ -15,13 +15,70 @@ module Git
15
15
  ref = @args.shift || "HEAD"
16
16
 
17
17
  repo = Repository.new
18
- require_database(repo)
19
-
20
- Database.connect(repo.git_dir)
18
+ use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
21
19
 
22
20
  sha = repo.rev_parse(ref)
23
21
  error "Could not resolve '#{ref}'. Check that the ref exists with 'git rev-parse #{ref}'." unless sha
24
22
 
23
+ if use_stateless
24
+ run_stateless(repo, sha)
25
+ else
26
+ run_with_database(repo, sha)
27
+ end
28
+ end
29
+
30
+ def run_stateless(repo, sha)
31
+ rugged_commit = repo.lookup(sha)
32
+ analyzer = Analyzer.new(repo)
33
+
34
+ if rugged_commit.parents.empty?
35
+ # First commit - all deps are "added"
36
+ deps = analyzer.dependencies_at_commit(rugged_commit)
37
+ changes = deps.map do |dep|
38
+ {
39
+ name: dep[:name],
40
+ change_type: "added",
41
+ requirement: dep[:requirement],
42
+ ecosystem: dep[:ecosystem],
43
+ manifest_path: dep[:manifest_path]
44
+ }
45
+ end
46
+ else
47
+ diff = analyzer.diff_commits(rugged_commit.parents[0], rugged_commit)
48
+ changes = []
49
+ diff[:added].each { |d| changes << d.merge(change_type: "added") }
50
+ diff[:modified].each { |d| changes << d.merge(change_type: "modified") }
51
+ diff[:removed].each { |d| changes << d.merge(change_type: "removed") }
52
+ end
53
+
54
+ if @options[:ecosystem]
55
+ changes = changes.select { |c| c[:ecosystem] == @options[:ecosystem] }
56
+ end
57
+
58
+ commit_info = {
59
+ sha: sha,
60
+ short_sha: sha[0..7],
61
+ message: rugged_commit.message,
62
+ author_name: rugged_commit.author[:name],
63
+ author_email: rugged_commit.author[:email],
64
+ committed_at: rugged_commit.time
65
+ }
66
+
67
+ if changes.empty?
68
+ empty_result "No dependency changes in #{commit_info[:short_sha]}"
69
+ return
70
+ end
71
+
72
+ if @options[:format] == "json"
73
+ output_json_stateless(commit_info, changes)
74
+ else
75
+ paginate { output_text_stateless(commit_info, changes) }
76
+ end
77
+ end
78
+
79
+ def run_with_database(repo, sha)
80
+ Database.connect(repo.git_dir)
81
+
25
82
  commit = Models::Commit.find_or_create_from_repo(repo, sha)
26
83
  error "Commit '#{sha[0..7]}' not in database. Run 'git pkgs update' to index new commits." unless commit
27
84
 
@@ -109,6 +166,68 @@ module Git
109
166
  puts JSON.pretty_generate(data)
110
167
  end
111
168
 
169
+ def output_text_stateless(commit_info, changes)
170
+ puts "Commit: #{commit_info[:short_sha]} #{commit_info[:message]&.lines&.first&.strip}"
171
+ puts "Author: #{commit_info[:author_name]} <#{commit_info[:author_email]}>"
172
+ puts "Date: #{commit_info[:committed_at].strftime("%Y-%m-%d")}"
173
+ puts
174
+
175
+ added = changes.select { |c| c[:change_type] == "added" }
176
+ modified = changes.select { |c| c[:change_type] == "modified" }
177
+ removed = changes.select { |c| c[:change_type] == "removed" }
178
+
179
+ if added.any?
180
+ puts Color.green("Added:")
181
+ added.each do |change|
182
+ puts Color.green(" + #{change[:name]} #{change[:requirement]} (#{change[:ecosystem]}, #{change[:manifest_path]})")
183
+ end
184
+ puts
185
+ end
186
+
187
+ if modified.any?
188
+ puts Color.yellow("Modified:")
189
+ modified.each do |change|
190
+ puts Color.yellow(" ~ #{change[:name]} #{change[:previous_requirement]} -> #{change[:requirement]} (#{change[:ecosystem]}, #{change[:manifest_path]})")
191
+ end
192
+ puts
193
+ end
194
+
195
+ if removed.any?
196
+ puts Color.red("Removed:")
197
+ removed.each do |change|
198
+ puts Color.red(" - #{change[:name]} #{change[:requirement]} (#{change[:ecosystem]}, #{change[:manifest_path]})")
199
+ end
200
+ puts
201
+ end
202
+ end
203
+
204
+ def output_json_stateless(commit_info, changes)
205
+ require "json"
206
+
207
+ data = {
208
+ commit: {
209
+ sha: commit_info[:sha],
210
+ short_sha: commit_info[:short_sha],
211
+ message: commit_info[:message]&.lines&.first&.strip,
212
+ author_name: commit_info[:author_name],
213
+ author_email: commit_info[:author_email],
214
+ date: commit_info[:committed_at].iso8601
215
+ },
216
+ changes: changes.map do |change|
217
+ {
218
+ name: change[:name],
219
+ change_type: change[:change_type],
220
+ requirement: change[:requirement],
221
+ previous_requirement: change[:previous_requirement],
222
+ ecosystem: change[:ecosystem],
223
+ manifest: change[:manifest_path]
224
+ }
225
+ end
226
+ }
227
+
228
+ puts JSON.pretty_generate(data)
229
+ end
230
+
112
231
  def parse_options
113
232
  options = {}
114
233
 
@@ -127,6 +246,10 @@ module Git
127
246
  options[:no_pager] = true
128
247
  end
129
248
 
249
+ opts.on("--stateless", "Parse manifests directly without database") do
250
+ options[:stateless] = true
251
+ end
252
+
130
253
  opts.on("-h", "--help", "Show this help") do
131
254
  puts opts
132
255
  exit
@@ -26,10 +26,14 @@ module Git
26
26
 
27
27
  return empty_result("No dependencies found") unless current_commit
28
28
 
29
- snapshots = current_commit.dependency_snapshots_dataset.eager(:manifest)
29
+ # Only look at lockfile dependencies (actual resolved versions, not constraints)
30
+ snapshots = current_commit.dependency_snapshots_dataset
31
+ .eager(:manifest)
32
+ .join(:manifests, id: :manifest_id)
33
+ .where(Sequel[:manifests][:kind] => "lockfile")
30
34
 
31
35
  if @options[:ecosystem]
32
- snapshots = snapshots.where(ecosystem: @options[:ecosystem])
36
+ snapshots = snapshots.where(Sequel[:dependency_snapshots][:ecosystem] => @options[:ecosystem])
33
37
  end
34
38
 
35
39
  snapshots = snapshots.all
@@ -41,6 +41,7 @@ module Git
41
41
  key = [s.manifest.path, s.name]
42
42
  snapshot[key] = {
43
43
  ecosystem: s.ecosystem,
44
+ purl: s.purl,
44
45
  requirement: s.requirement,
45
46
  dependency_type: s.dependency_type
46
47
  }
@@ -88,6 +89,7 @@ module Git
88
89
  manifest: manifest,
89
90
  name: change[:name],
90
91
  ecosystem: change[:ecosystem],
92
+ purl: change[:purl],
91
93
  change_type: change[:change_type],
92
94
  requirement: change[:requirement],
93
95
  previous_requirement: change[:previous_requirement],
@@ -105,6 +107,7 @@ module Git
105
107
  name: name
106
108
  ) do |s|
107
109
  s.ecosystem = dep_info[:ecosystem]
110
+ s.purl = dep_info[:purl]
108
111
  s.requirement = dep_info[:requirement]
109
112
  s.dependency_type = dep_info[:dependency_type]
110
113
  end