git-pkgs 0.7.0 → 0.9.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/CHANGELOG.md +25 -0
- data/Formula/git-pkgs.rb +2 -2
- data/README.md +84 -5
- data/lib/git/pkgs/analyzer.rb +19 -10
- data/lib/git/pkgs/cli.rb +6 -2
- data/lib/git/pkgs/commands/branch.rb +5 -2
- data/lib/git/pkgs/commands/diff_driver.rb +6 -0
- data/lib/git/pkgs/commands/init.rb +5 -2
- data/lib/git/pkgs/commands/integrity.rb +288 -0
- data/lib/git/pkgs/commands/licenses.rb +416 -0
- data/lib/git/pkgs/commands/outdated.rb +312 -0
- data/lib/git/pkgs/commands/sbom.rb +325 -0
- data/lib/git/pkgs/commands/update.rb +3 -1
- data/lib/git/pkgs/commands/vulns/base.rb +16 -12
- data/lib/git/pkgs/commands/vulns/diff.rb +3 -2
- data/lib/git/pkgs/commands/vulns/praise.rb +2 -0
- data/lib/git/pkgs/commands/vulns/sync.rb +30 -28
- data/lib/git/pkgs/config.rb +1 -2
- data/lib/git/pkgs/database.rb +23 -13
- data/lib/git/pkgs/ecosystems_client.rb +142 -0
- data/lib/git/pkgs/models/dependency_change.rb +8 -0
- data/lib/git/pkgs/models/dependency_snapshot.rb +8 -0
- data/lib/git/pkgs/models/package.rb +61 -0
- data/lib/git/pkgs/models/version.rb +56 -0
- data/lib/git/pkgs/purl_helper.rb +56 -0
- data/lib/git/pkgs/spinner.rb +46 -0
- data/lib/git/pkgs/version.rb +1 -1
- data/lib/git/pkgs.rb +9 -0
- metadata +45 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3bd1b968e2bc7ed2d3ecbc8a750925afdea362ca1d0a01f6d69a6bf5aef58b02
|
|
4
|
+
data.tar.gz: 1deed6e6742a7977585c97db65fe13e649e4b5e3b6a0f9674f5352d977dc8fcf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d7ed904f5e7b3fc0b7a7054d596dfe59d1918f30d493ac5613cea125c41f9b38ab22daab2c84a5536b55030ca407334b01e6d0bd79bc828a371abfdccf5a16d
|
|
7
|
+
data.tar.gz: e1170c4c9eeb345730e9ab614536c6431f1c56ee9bafc8bdff111e7df3bf8f577149ba1f94a77787ace083ee401b2980978148f76e85d16c5a3a1101902a2a08
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.9.0] - 2026-01-14
|
|
4
|
+
|
|
5
|
+
- `git pkgs sbom` command to export dependencies as SPDX or CycloneDX
|
|
6
|
+
- `git pkgs integrity` command to show and verify lockfile integrity hashes
|
|
7
|
+
- Parse go.sum for Go module integrity hashes (no longer ignored)
|
|
8
|
+
- Convert Go h1: hashes (base64) to hex for SBOM compatibility
|
|
9
|
+
- `--drift` flag to detect packages with different hashes for the same version
|
|
10
|
+
- Registry integrity comparison via ecosyste.ms API
|
|
11
|
+
- Store integrity hashes from lockfiles in dependency_snapshots table
|
|
12
|
+
- SBOM export includes supplier info from ecosyste.ms (owner/maintainer)
|
|
13
|
+
- License commands use version-level license data when available
|
|
14
|
+
- Store supplier_name and supplier_type on packages (schema v5, run `git pkgs upgrade`)
|
|
15
|
+
- Update ecosystems-bibliothecary to ~> 15.3 (integrity extraction from lockfiles)
|
|
16
|
+
- Update purl to >= 1.7.1 (ecosyste.ms API URL support)
|
|
17
|
+
|
|
18
|
+
## [0.8.0] - 2026-01-14
|
|
19
|
+
|
|
20
|
+
- `git pkgs outdated` command to find dependencies with newer versions available in registries
|
|
21
|
+
- `git pkgs licenses` command to check dependency licenses with compliance options (--permissive, --allow, --deny)
|
|
22
|
+
- ecosyste.ms client for fetching package metadata (latest versions, licenses)
|
|
23
|
+
- Package and Version models for storing enrichment data
|
|
24
|
+
- Spinner utility for progress feedback during network operations
|
|
25
|
+
- PURL helper for standardized package URLs
|
|
26
|
+
- `outdated` is no longer an alias for `stale` (now a separate command)
|
|
27
|
+
|
|
3
28
|
## [0.7.0] - 2026-01-09
|
|
4
29
|
|
|
5
30
|
- `git pkgs vulns` subcommand for vulnerability scanning via OSV API
|
data/Formula/git-pkgs.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
class GitPkgs < Formula
|
|
2
2
|
desc "Track package dependencies across git history"
|
|
3
3
|
homepage "https://github.com/andrew/git-pkgs"
|
|
4
|
-
url "https://github.com/andrew/git-pkgs/archive/refs/tags/v0.
|
|
5
|
-
sha256 "
|
|
4
|
+
url "https://github.com/andrew/git-pkgs/archive/refs/tags/v0.8.0.tar.gz"
|
|
5
|
+
sha256 "b2e8ebfefc86fd137fb76225934c0fe2a1e01b2fcaac88a91f1134eecc4e9e71"
|
|
6
6
|
license "AGPL-3.0"
|
|
7
7
|
|
|
8
8
|
depends_on "cmake" => :build
|
data/README.md
CHANGED
|
@@ -10,7 +10,9 @@ Your lockfile shows what dependencies you have, but it doesn't show how you got
|
|
|
10
10
|
|
|
11
11
|
For best results, commit your lockfiles. Manifests show version ranges but lockfiles show what actually got installed, including transitive dependencies.
|
|
12
12
|
|
|
13
|
-
It works across many ecosystems (Gemfile, package.json, Dockerfile, GitHub Actions workflows) giving you one unified history instead of separate tools per ecosystem.
|
|
13
|
+
It works across many ecosystems (Gemfile, package.json, Dockerfile, GitHub Actions workflows) giving you one unified history instead of separate tools per ecosystem. The database lives in your `.git` directory where you can use it in CI to catch dependency changes in pull requests.
|
|
14
|
+
|
|
15
|
+
The core commands (`list`, `history`, `blame`, `diff`, `stale`, etc.) work entirely from your git history with no network access. Additional commands fetch external data: `vulns` checks [OSV](https://osv.dev) for known CVEs, while `outdated` and `licenses` query [ecosyste.ms](https://packages.ecosyste.ms/) for registry metadata. See [docs/enrichment.md](docs/enrichment.md) for details on external data.
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
16
18
|
|
|
@@ -45,6 +47,8 @@ git pkgs history rails # track a specific package
|
|
|
45
47
|
git pkgs why rails # why was this added?
|
|
46
48
|
git pkgs diff --from=HEAD~10 # what changed recently?
|
|
47
49
|
git pkgs diff --from=main --to=feature # compare branches
|
|
50
|
+
git pkgs vulns # scan for known CVEs
|
|
51
|
+
git pkgs vulns blame # who introduced each vulnerability
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
## Commands
|
|
@@ -254,22 +258,96 @@ This shows dependencies grouped by type (runtime, development, etc).
|
|
|
254
258
|
git pkgs stale # list deps by how long since last touched
|
|
255
259
|
git pkgs stale --days=365 # only show deps untouched for a year
|
|
256
260
|
git pkgs stale --ecosystem=npm # filter by ecosystem
|
|
257
|
-
git pkgs outdated # alias for stale
|
|
258
261
|
```
|
|
259
262
|
|
|
260
263
|
Shows dependencies sorted by how long since they were last changed in your repo. Useful for finding packages that may have been forgotten or need review.
|
|
261
264
|
|
|
265
|
+
### Find outdated dependencies
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
git pkgs outdated # show packages with newer versions available
|
|
269
|
+
git pkgs outdated --major # only major version updates
|
|
270
|
+
git pkgs outdated --minor # minor and major updates (skip patch)
|
|
271
|
+
git pkgs outdated --stateless # no database needed
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Checks package registries (via [ecosyste.ms](https://packages.ecosyste.ms/)) to find dependencies with newer versions available. Major updates are shown in red, minor in yellow, patch in cyan.
|
|
275
|
+
|
|
276
|
+
### Check licenses
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
git pkgs licenses # show license for each dependency
|
|
280
|
+
git pkgs licenses --permissive # flag copyleft licenses
|
|
281
|
+
git pkgs licenses --allow=MIT,Apache-2.0 # explicit allow list
|
|
282
|
+
git pkgs licenses --group # group output by license
|
|
283
|
+
git pkgs licenses --stateless # no database needed
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Fetches license information from package registries. Exits with code 1 if violations are found, making it suitable for CI. See [docs/enrichment.md](docs/enrichment.md) for all options.
|
|
287
|
+
|
|
262
288
|
### Vulnerability scanning
|
|
263
289
|
|
|
290
|
+
Scan dependencies for known CVEs using the [OSV database](https://osv.dev). Because git-pkgs tracks the full history of every dependency change, it provides context that static scanners can't: who introduced a vulnerability, when it was fixed, and how long you were exposed.
|
|
291
|
+
|
|
264
292
|
```bash
|
|
265
|
-
git pkgs vulns # scan current dependencies
|
|
293
|
+
git pkgs vulns # scan current dependencies
|
|
294
|
+
git pkgs vulns v1.0.0 # scan at a tag, branch, or commit
|
|
266
295
|
git pkgs vulns -s high # only critical and high severity
|
|
296
|
+
git pkgs vulns -e npm # filter by ecosystem
|
|
297
|
+
git pkgs vulns -f sarif # output for GitHub code scanning
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Subcommands for historical analysis:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
267
303
|
git pkgs vulns blame # who introduced each vulnerability
|
|
304
|
+
git pkgs vulns blame --all-time # include fixed vulnerabilities
|
|
268
305
|
git pkgs vulns praise # who fixed vulnerabilities
|
|
269
|
-
git pkgs vulns
|
|
306
|
+
git pkgs vulns praise --summary # author leaderboard
|
|
307
|
+
git pkgs vulns exposure # remediation metrics (CRA compliance)
|
|
308
|
+
git pkgs vulns diff main feature # compare vulnerability state between refs
|
|
309
|
+
git pkgs vulns log # commits that introduced or fixed vulns
|
|
310
|
+
git pkgs vulns history lodash # vulnerability timeline for a package
|
|
311
|
+
git pkgs vulns show CVE-2024-1234 # details about a specific CVE
|
|
270
312
|
```
|
|
271
313
|
|
|
272
|
-
|
|
314
|
+
Output formats: `text` (default), `json`, and `sarif`. SARIF integrates with GitHub Advanced Security:
|
|
315
|
+
|
|
316
|
+
```yaml
|
|
317
|
+
- run: git pkgs vulns --stateless -f sarif > results.sarif
|
|
318
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
319
|
+
with:
|
|
320
|
+
sarif_file: results.sarif
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Vulnerability data is cached locally and refreshed automatically when stale (>24h). Use `git pkgs vulns sync --refresh` to force an update. See [docs/vulns.md](docs/vulns.md) for full documentation.
|
|
324
|
+
|
|
325
|
+
### Integrity verification
|
|
326
|
+
|
|
327
|
+
Show SHA256 hashes from lockfiles. Modern lockfiles include checksums that verify package contents haven't been tampered with.
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
git pkgs integrity # show hashes for current dependencies
|
|
331
|
+
git pkgs integrity --drift # detect same version with different hashes
|
|
332
|
+
git pkgs integrity -f json # JSON output
|
|
333
|
+
git pkgs integrity --stateless # no database needed
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The `--drift` flag scans your history for packages where the same version has different integrity hashes, which could indicate a supply chain issue.
|
|
337
|
+
|
|
338
|
+
### SBOM export
|
|
339
|
+
|
|
340
|
+
Export dependencies as a Software Bill of Materials in CycloneDX or SPDX format:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
git pkgs sbom # CycloneDX JSON (default)
|
|
344
|
+
git pkgs sbom --type spdx # SPDX JSON
|
|
345
|
+
git pkgs sbom -f xml # XML instead of JSON
|
|
346
|
+
git pkgs sbom --name my-project # custom project name
|
|
347
|
+
git pkgs sbom --stateless # no database needed
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Includes package URLs (purls), versions, and licenses (fetched from registries). Use `--skip-enrichment` to omit license lookups.
|
|
273
351
|
|
|
274
352
|
### Diff between commits
|
|
275
353
|
|
|
@@ -497,6 +575,7 @@ Git::Pkgs::Database.connect(repo_git_dir)
|
|
|
497
575
|
Git::Pkgs::Models::DependencyChange.where(name: "rails").all
|
|
498
576
|
```
|
|
499
577
|
|
|
578
|
+
|
|
500
579
|
## Contributing
|
|
501
580
|
|
|
502
581
|
Bug reports, feature requests, and pull requests are welcome. If you're unsure about a change, open an issue first to discuss it.
|
data/lib/git/pkgs/analyzer.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Git
|
|
|
33
33
|
REQUIRE Project.toml Manifest.toml
|
|
34
34
|
shard.yml shard.lock
|
|
35
35
|
elm-package.json elm_dependencies.json elm-stuff/exact-dependencies.json
|
|
36
|
-
haxelib.json
|
|
36
|
+
haxelib.json stack.yaml stack.yaml.lock
|
|
37
37
|
action.yml action.yaml .github/workflows/*.yml .github/workflows/*.yaml
|
|
38
38
|
Dockerfile docker-compose*.yml docker-compose*.yaml
|
|
39
39
|
dvc.yaml vcpkg.json _generated-vcpkg-list.json
|
|
@@ -141,7 +141,8 @@ module Git
|
|
|
141
141
|
purl: generate_purl(result[:platform], dep[:name]),
|
|
142
142
|
change_type: "added",
|
|
143
143
|
requirement: dep[:requirement],
|
|
144
|
-
dependency_type: dep[:type]
|
|
144
|
+
dependency_type: dep[:type],
|
|
145
|
+
integrity: dep[:integrity]
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
key = [manifest_path, dep[:name]]
|
|
@@ -150,7 +151,8 @@ module Git
|
|
|
150
151
|
kind: result[:kind],
|
|
151
152
|
purl: generate_purl(result[:platform], dep[:name]),
|
|
152
153
|
requirement: dep[:requirement],
|
|
153
|
-
dependency_type: dep[:type]
|
|
154
|
+
dependency_type: dep[:type],
|
|
155
|
+
integrity: dep[:integrity]
|
|
154
156
|
}
|
|
155
157
|
end
|
|
156
158
|
end
|
|
@@ -179,7 +181,8 @@ module Git
|
|
|
179
181
|
purl: generate_purl(after_result[:platform], name),
|
|
180
182
|
change_type: "added",
|
|
181
183
|
requirement: dep[:requirement],
|
|
182
|
-
dependency_type: dep[:type]
|
|
184
|
+
dependency_type: dep[:type],
|
|
185
|
+
integrity: dep[:integrity]
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
key = [manifest_path, name]
|
|
@@ -188,7 +191,8 @@ module Git
|
|
|
188
191
|
kind: after_result[:kind],
|
|
189
192
|
purl: generate_purl(after_result[:platform], name),
|
|
190
193
|
requirement: dep[:requirement],
|
|
191
|
-
dependency_type: dep[:type]
|
|
194
|
+
dependency_type: dep[:type],
|
|
195
|
+
integrity: dep[:integrity]
|
|
192
196
|
}
|
|
193
197
|
end
|
|
194
198
|
|
|
@@ -202,7 +206,8 @@ module Git
|
|
|
202
206
|
purl: generate_purl(before_result[:platform], name),
|
|
203
207
|
change_type: "removed",
|
|
204
208
|
requirement: dep[:requirement],
|
|
205
|
-
dependency_type: dep[:type]
|
|
209
|
+
dependency_type: dep[:type],
|
|
210
|
+
integrity: dep[:integrity]
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
key = [manifest_path, name]
|
|
@@ -223,7 +228,8 @@ module Git
|
|
|
223
228
|
change_type: "modified",
|
|
224
229
|
requirement: after_dep[:requirement],
|
|
225
230
|
previous_requirement: before_dep[:requirement],
|
|
226
|
-
dependency_type: after_dep[:type]
|
|
231
|
+
dependency_type: after_dep[:type],
|
|
232
|
+
integrity: after_dep[:integrity]
|
|
227
233
|
}
|
|
228
234
|
|
|
229
235
|
key = [manifest_path, name]
|
|
@@ -232,7 +238,8 @@ module Git
|
|
|
232
238
|
kind: after_result[:kind],
|
|
233
239
|
purl: generate_purl(after_result[:platform], name),
|
|
234
240
|
requirement: after_dep[:requirement],
|
|
235
|
-
dependency_type: after_dep[:type]
|
|
241
|
+
dependency_type: after_dep[:type],
|
|
242
|
+
integrity: after_dep[:integrity]
|
|
236
243
|
}
|
|
237
244
|
end
|
|
238
245
|
end
|
|
@@ -252,7 +259,8 @@ module Git
|
|
|
252
259
|
purl: generate_purl(result[:platform], dep[:name]),
|
|
253
260
|
change_type: "removed",
|
|
254
261
|
requirement: dep[:requirement],
|
|
255
|
-
dependency_type: dep[:type]
|
|
262
|
+
dependency_type: dep[:type],
|
|
263
|
+
integrity: dep[:integrity]
|
|
256
264
|
}
|
|
257
265
|
|
|
258
266
|
key = [manifest_path, dep[:name]]
|
|
@@ -329,7 +337,8 @@ module Git
|
|
|
329
337
|
kind: result[:kind],
|
|
330
338
|
purl: generate_purl(result[:platform], dep[:name]),
|
|
331
339
|
requirement: dep[:requirement],
|
|
332
|
-
dependency_type: dep[:type]
|
|
340
|
+
dependency_type: dep[:type],
|
|
341
|
+
integrity: dep[:integrity]
|
|
333
342
|
}
|
|
334
343
|
end
|
|
335
344
|
end
|
data/lib/git/pkgs/cli.rb
CHANGED
|
@@ -33,7 +33,11 @@ module Git
|
|
|
33
33
|
},
|
|
34
34
|
"Analysis" => {
|
|
35
35
|
"stats" => "Show dependency statistics",
|
|
36
|
-
"stale" => "Show dependencies that haven't been updated"
|
|
36
|
+
"stale" => "Show dependencies that haven't been updated",
|
|
37
|
+
"outdated" => "Show packages with newer versions available",
|
|
38
|
+
"licenses" => "Show licenses for dependencies",
|
|
39
|
+
"integrity" => "Show and verify lockfile integrity hashes",
|
|
40
|
+
"sbom" => "Export dependencies as SBOM (SPDX or CycloneDX)"
|
|
37
41
|
},
|
|
38
42
|
"Security" => {
|
|
39
43
|
"vulns" => "Scan for known vulnerabilities"
|
|
@@ -42,7 +46,7 @@ module Git
|
|
|
42
46
|
|
|
43
47
|
COMMANDS = COMMAND_GROUPS.values.flat_map(&:keys).freeze
|
|
44
48
|
COMMAND_DESCRIPTIONS = COMMAND_GROUPS.values.reduce({}, :merge).freeze
|
|
45
|
-
ALIASES = { "praise" => "blame"
|
|
49
|
+
ALIASES = { "praise" => "blame" }.freeze
|
|
46
50
|
|
|
47
51
|
def self.run(args)
|
|
48
52
|
new(args).run
|
|
@@ -191,6 +191,7 @@ module Git
|
|
|
191
191
|
ecosystem: s[:ecosystem],
|
|
192
192
|
requirement: s[:requirement],
|
|
193
193
|
dependency_type: s[:dependency_type],
|
|
194
|
+
integrity: s[:integrity],
|
|
194
195
|
created_at: now,
|
|
195
196
|
updated_at: now
|
|
196
197
|
}
|
|
@@ -266,7 +267,8 @@ module Git
|
|
|
266
267
|
name: name,
|
|
267
268
|
ecosystem: dep_info[:ecosystem],
|
|
268
269
|
requirement: dep_info[:requirement],
|
|
269
|
-
dependency_type: dep_info[:dependency_type]
|
|
270
|
+
dependency_type: dep_info[:dependency_type],
|
|
271
|
+
integrity: dep_info[:integrity]
|
|
270
272
|
}
|
|
271
273
|
end
|
|
272
274
|
snapshots_stored += snapshot.size
|
|
@@ -286,7 +288,8 @@ module Git
|
|
|
286
288
|
name: name,
|
|
287
289
|
ecosystem: dep_info[:ecosystem],
|
|
288
290
|
requirement: dep_info[:requirement],
|
|
289
|
-
dependency_type: dep_info[:dependency_type]
|
|
291
|
+
dependency_type: dep_info[:dependency_type],
|
|
292
|
+
integrity: dep_info[:integrity]
|
|
290
293
|
}
|
|
291
294
|
end
|
|
292
295
|
snapshots_stored += snapshot.size
|
|
@@ -24,18 +24,24 @@ module Git
|
|
|
24
24
|
gems.locked
|
|
25
25
|
glide.lock
|
|
26
26
|
go.mod
|
|
27
|
+
go.sum
|
|
28
|
+
gradle.lockfile
|
|
27
29
|
mix.lock
|
|
28
30
|
npm-shrinkwrap.json
|
|
29
31
|
package-lock.json
|
|
30
32
|
packages.lock.json
|
|
31
33
|
paket.lock
|
|
34
|
+
pdm.lock
|
|
32
35
|
pnpm-lock.yaml
|
|
33
36
|
poetry.lock
|
|
34
37
|
project.assets.json
|
|
35
38
|
pubspec.lock
|
|
36
39
|
pylock.toml
|
|
40
|
+
renv.lock
|
|
37
41
|
shard.lock
|
|
42
|
+
stack.yaml.lock
|
|
38
43
|
uv.lock
|
|
44
|
+
verification-metadata.xml
|
|
39
45
|
yarn.lock
|
|
40
46
|
].freeze
|
|
41
47
|
|
|
@@ -125,6 +125,7 @@ module Git
|
|
|
125
125
|
purl: s[:purl],
|
|
126
126
|
requirement: s[:requirement],
|
|
127
127
|
dependency_type: s[:dependency_type],
|
|
128
|
+
integrity: s[:integrity],
|
|
128
129
|
created_at: now,
|
|
129
130
|
updated_at: now
|
|
130
131
|
}
|
|
@@ -207,7 +208,8 @@ module Git
|
|
|
207
208
|
ecosystem: dep_info[:ecosystem],
|
|
208
209
|
purl: dep_info[:purl],
|
|
209
210
|
requirement: dep_info[:requirement],
|
|
210
|
-
dependency_type: dep_info[:dependency_type]
|
|
211
|
+
dependency_type: dep_info[:dependency_type],
|
|
212
|
+
integrity: dep_info[:integrity]
|
|
211
213
|
}
|
|
212
214
|
end
|
|
213
215
|
snapshots_stored += snapshot.size
|
|
@@ -228,7 +230,8 @@ module Git
|
|
|
228
230
|
ecosystem: dep_info[:ecosystem],
|
|
229
231
|
purl: dep_info[:purl],
|
|
230
232
|
requirement: dep_info[:requirement],
|
|
231
|
-
dependency_type: dep_info[:dependency_type]
|
|
233
|
+
dependency_type: dep_info[:dependency_type],
|
|
234
|
+
integrity: dep_info[:integrity]
|
|
232
235
|
}
|
|
233
236
|
end
|
|
234
237
|
snapshots_stored += snapshot.size
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
class Integrity
|
|
7
|
+
include Output
|
|
8
|
+
|
|
9
|
+
def self.description
|
|
10
|
+
"Show and verify lockfile integrity hashes"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(args)
|
|
14
|
+
@args = args
|
|
15
|
+
@options = parse_options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
repo = Repository.new
|
|
20
|
+
use_stateless = @options[:stateless] || !Database.exists?(repo.git_dir)
|
|
21
|
+
|
|
22
|
+
if @options[:drift]
|
|
23
|
+
error "--drift requires database (run 'git pkgs init' first)" if use_stateless
|
|
24
|
+
run_drift_detection(repo)
|
|
25
|
+
else
|
|
26
|
+
run_show(repo, use_stateless)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def run_show(repo, use_stateless)
|
|
31
|
+
if use_stateless
|
|
32
|
+
deps = run_stateless(repo)
|
|
33
|
+
else
|
|
34
|
+
deps = run_with_database(repo)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Filter to only lockfile deps with integrity
|
|
38
|
+
deps = Analyzer.lockfile_dependencies(deps)
|
|
39
|
+
deps = deps.select { |d| d[:integrity] }
|
|
40
|
+
|
|
41
|
+
if @options[:ecosystem]
|
|
42
|
+
deps = deps.select { |d| d[:ecosystem] == @options[:ecosystem] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if deps.empty?
|
|
46
|
+
empty_result "No dependencies with integrity hashes found"
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if @options[:format] == "json"
|
|
51
|
+
require "json"
|
|
52
|
+
puts JSON.pretty_generate(deps.map { |d| format_dep_json(d) })
|
|
53
|
+
else
|
|
54
|
+
paginate { output_text(deps) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def run_stateless(repo)
|
|
59
|
+
commit_sha = @options[:ref] || repo.head_sha
|
|
60
|
+
rugged_commit = repo.lookup(repo.rev_parse(commit_sha))
|
|
61
|
+
error "Could not resolve '#{commit_sha}'" unless rugged_commit
|
|
62
|
+
|
|
63
|
+
analyzer = Analyzer.new(repo)
|
|
64
|
+
analyzer.dependencies_at_commit(rugged_commit)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def run_with_database(repo)
|
|
68
|
+
Database.connect(repo.git_dir)
|
|
69
|
+
|
|
70
|
+
commit_sha = @options[:ref] || repo.head_sha
|
|
71
|
+
target_commit = Models::Commit.first(sha: commit_sha)
|
|
72
|
+
error "Commit not in database. Run 'git pkgs update' first." unless target_commit
|
|
73
|
+
|
|
74
|
+
compute_dependencies_at_commit(target_commit, repo)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def run_drift_detection(repo)
|
|
78
|
+
Database.connect(repo.git_dir)
|
|
79
|
+
|
|
80
|
+
# Get unique (purl, requirement, integrity) from snapshots
|
|
81
|
+
results = Database.db[:dependency_snapshots]
|
|
82
|
+
.exclude(integrity: nil)
|
|
83
|
+
.select(:purl, :requirement, :integrity)
|
|
84
|
+
.distinct
|
|
85
|
+
.all
|
|
86
|
+
|
|
87
|
+
# Build versioned purls and group
|
|
88
|
+
by_versioned_purl = {}
|
|
89
|
+
results.each do |r|
|
|
90
|
+
versioned_purl = "#{r[:purl]}@#{r[:requirement]}"
|
|
91
|
+
by_versioned_purl[versioned_purl] ||= { purl: r[:purl], version: r[:requirement], lockfile_integrities: [] }
|
|
92
|
+
by_versioned_purl[versioned_purl][:lockfile_integrities] << r[:integrity]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Dedupe lockfile integrities
|
|
96
|
+
by_versioned_purl.each { |_, v| v[:lockfile_integrities].uniq! }
|
|
97
|
+
|
|
98
|
+
# Find internal drift (same version with different lockfile hashes)
|
|
99
|
+
internal_drifts = by_versioned_purl.select { |_, v| v[:lockfile_integrities].size > 1 }
|
|
100
|
+
|
|
101
|
+
# Fetch registry integrity for comparison
|
|
102
|
+
registry_mismatches = []
|
|
103
|
+
purls_to_check = by_versioned_purl.keys
|
|
104
|
+
|
|
105
|
+
if purls_to_check.any?
|
|
106
|
+
Spinner.with_spinner("Fetching registry integrity...") do
|
|
107
|
+
client = EcosystemsClient.new
|
|
108
|
+
purls_to_check.each do |versioned_purl|
|
|
109
|
+
data = by_versioned_purl[versioned_purl]
|
|
110
|
+
version_info = client.lookup_version(versioned_purl)
|
|
111
|
+
next unless version_info && version_info["integrity"]
|
|
112
|
+
|
|
113
|
+
registry_integrity = version_info["integrity"]
|
|
114
|
+
lockfile_integrity = data[:lockfile_integrities].first
|
|
115
|
+
|
|
116
|
+
unless integrity_match?(lockfile_integrity, registry_integrity)
|
|
117
|
+
registry_mismatches << {
|
|
118
|
+
purl: versioned_purl,
|
|
119
|
+
lockfile: lockfile_integrity,
|
|
120
|
+
registry: registry_integrity
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if internal_drifts.empty? && registry_mismatches.empty?
|
|
128
|
+
info "No integrity drift detected"
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if @options[:format] == "json"
|
|
133
|
+
require "json"
|
|
134
|
+
output = {
|
|
135
|
+
internal_drift: internal_drifts.map { |purl, v| { purl: purl, integrity_values: v[:lockfile_integrities] } },
|
|
136
|
+
registry_mismatch: registry_mismatches
|
|
137
|
+
}
|
|
138
|
+
puts JSON.pretty_generate(output)
|
|
139
|
+
else
|
|
140
|
+
paginate { output_drift_text(internal_drifts, registry_mismatches) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def integrity_match?(lockfile, registry)
|
|
145
|
+
normalize_integrity(lockfile) == normalize_integrity(registry)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def normalize_integrity(integrity)
|
|
149
|
+
return nil unless integrity
|
|
150
|
+
# Normalize sha256= vs sha256- format
|
|
151
|
+
integrity.gsub(/^sha256[-=]/, "sha256:")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def output_text(deps)
|
|
155
|
+
grouped = deps.group_by { |d| d[:ecosystem] }
|
|
156
|
+
|
|
157
|
+
grouped.each do |ecosystem, ecosystem_deps|
|
|
158
|
+
puts "#{ecosystem}:"
|
|
159
|
+
ecosystem_deps.sort_by { |d| d[:name] }.each do |dep|
|
|
160
|
+
puts " #{dep[:name]} #{dep[:requirement]}"
|
|
161
|
+
puts " #{dep[:integrity]}"
|
|
162
|
+
end
|
|
163
|
+
puts
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def output_drift_text(internal_drifts, registry_mismatches)
|
|
168
|
+
if internal_drifts.any?
|
|
169
|
+
puts Color.red("Internal drift (same version, different lockfile hashes):")
|
|
170
|
+
puts
|
|
171
|
+
internal_drifts.each do |purl, data|
|
|
172
|
+
puts " #{purl}"
|
|
173
|
+
data[:lockfile_integrities].each do |integrity|
|
|
174
|
+
puts " #{integrity}"
|
|
175
|
+
end
|
|
176
|
+
puts
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if registry_mismatches.any?
|
|
181
|
+
puts Color.red("Registry mismatch (lockfile differs from registry):")
|
|
182
|
+
puts
|
|
183
|
+
registry_mismatches.each do |mismatch|
|
|
184
|
+
puts " #{mismatch[:purl]}"
|
|
185
|
+
puts " lockfile: #{mismatch[:lockfile]}"
|
|
186
|
+
puts " registry: #{mismatch[:registry]}"
|
|
187
|
+
puts
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
total = internal_drifts.size + registry_mismatches.size
|
|
192
|
+
puts "#{total} integrity issue(s) found"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def format_dep_json(dep)
|
|
196
|
+
{
|
|
197
|
+
name: dep[:name],
|
|
198
|
+
version: dep[:requirement],
|
|
199
|
+
ecosystem: dep[:ecosystem],
|
|
200
|
+
purl: dep[:purl],
|
|
201
|
+
integrity: dep[:integrity],
|
|
202
|
+
manifest: dep[:manifest_path]
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def compute_dependencies_at_commit(target_commit, repo)
|
|
207
|
+
branch_name = @options[:branch] || repo.default_branch
|
|
208
|
+
branch = Models::Branch.first(name: branch_name)
|
|
209
|
+
return [] unless branch
|
|
210
|
+
|
|
211
|
+
snapshot_commit = branch.commits_dataset
|
|
212
|
+
.join(:dependency_snapshots, commit_id: :id)
|
|
213
|
+
.where { Sequel[:commits][:committed_at] <= target_commit.committed_at }
|
|
214
|
+
.order(Sequel.desc(Sequel[:commits][:committed_at]))
|
|
215
|
+
.distinct
|
|
216
|
+
.first
|
|
217
|
+
|
|
218
|
+
deps = {}
|
|
219
|
+
if snapshot_commit
|
|
220
|
+
snapshot_commit.dependency_snapshots.each do |s|
|
|
221
|
+
key = [s.manifest.path, s.name]
|
|
222
|
+
deps[key] = {
|
|
223
|
+
manifest_path: s.manifest.path,
|
|
224
|
+
name: s.name,
|
|
225
|
+
ecosystem: s.ecosystem,
|
|
226
|
+
kind: s.manifest.kind,
|
|
227
|
+
manifest_kind: s.manifest.kind,
|
|
228
|
+
purl: s.purl,
|
|
229
|
+
requirement: s.requirement,
|
|
230
|
+
dependency_type: s.dependency_type,
|
|
231
|
+
integrity: s.integrity
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
deps.values
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def parse_options
|
|
240
|
+
options = {}
|
|
241
|
+
|
|
242
|
+
parser = OptionParser.new do |opts|
|
|
243
|
+
opts.banner = "Usage: git pkgs integrity [options]"
|
|
244
|
+
opts.separator ""
|
|
245
|
+
opts.separator "Show integrity hashes from lockfiles. Hashes come from lockfile checksums"
|
|
246
|
+
opts.separator "(Gemfile.lock CHECKSUMS, package-lock.json integrity fields, etc.)"
|
|
247
|
+
|
|
248
|
+
opts.on("-r", "--ref=REF", "Git ref to check (default: HEAD)") do |v|
|
|
249
|
+
options[:ref] = v
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
253
|
+
options[:ecosystem] = v
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
opts.on("-b", "--branch=NAME", "Branch context for database queries") do |v|
|
|
257
|
+
options[:branch] = v
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
261
|
+
options[:format] = v
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
opts.on("--drift", "Detect packages with different hashes for same version") do
|
|
265
|
+
options[:drift] = true
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
opts.on("--stateless", "Parse manifests directly without database") do
|
|
269
|
+
options[:stateless] = true
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
opts.on("--no-pager", "Do not pipe output into a pager") do
|
|
273
|
+
options[:no_pager] = true
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
opts.on("-h", "--help", "Show this help") do
|
|
277
|
+
puts opts
|
|
278
|
+
exit
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
parser.parse!(@args)
|
|
283
|
+
options
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|