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
|
@@ -80,10 +80,10 @@ module Git
|
|
|
80
80
|
|
|
81
81
|
def install_driver
|
|
82
82
|
# Set up git config for textconv
|
|
83
|
-
|
|
83
|
+
git_config("diff.pkgs.textconv", "git-pkgs diff-driver")
|
|
84
84
|
|
|
85
85
|
# Add to .gitattributes
|
|
86
|
-
gitattributes_path = File.join(
|
|
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
|
-
|
|
112
|
+
git_config_unset("diff.pkgs.textconv")
|
|
113
113
|
|
|
114
|
-
gitattributes_path = File.join(
|
|
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
|
-
|
|
16
|
+
use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|