Skip to content
This repository was archived by the owner on Jan 22, 2026. It is now read-only.

Commit 6c4c84c

Browse files
committed
Add integrity verification command
- Introduced `git pkgs integrity` command to show and verify lockfile integrity hashes. - Added `--drift` flag to detect packages with different hashes for the same version. - Updated `ecosystems-bibliothecary` to ~> 15.3 and `purl` to >= 1.7.1 - Enhanced `dependency_snapshots` table to store integrity hashes (schema v4). - Updated documentation to include integrity verification details. - Modified various components to support integrity tracking and reporting. - Added tests for integrity command and lockfile integrity extraction.
1 parent eaec4d0 commit 6c4c84c

File tree

17 files changed

+633
-72
lines changed

17 files changed

+633
-72
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
## [Unreleased]
22

3+
- `git pkgs integrity` command to show and verify lockfile integrity hashes
4+
- `--drift` flag to detect packages with different hashes for the same version
5+
- Registry integrity comparison via ecosyste.ms API
6+
- Store integrity hashes from lockfiles in dependency_snapshots table (schema v4, run `git pkgs upgrade`)
7+
- Update ecosystems-bibliothecary to ~> 15.3 (integrity extraction from lockfiles)
8+
- Update purl to >= 1.7.1 (ecosyste.ms API URL support)
9+
310
## [0.8.0] - 2026-01-14
411

512
- `git pkgs outdated` command to find dependencies with newer versions available in registries

Gemfile.lock

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ PATH
88
remote: .
99
specs:
1010
git-pkgs (0.8.0)
11-
ecosystems-bibliothecary (~> 15.2)
12-
purl (~> 1.7)
11+
ecosystems-bibliothecary (~> 15.3)
12+
purl (~> 1.7, >= 1.7.1)
1313
rugged (~> 1.0)
1414
sarif-ruby
1515
sequel (>= 5.0)
@@ -29,7 +29,7 @@ GEM
2929
csv (3.3.5)
3030
date (3.5.1)
3131
docile (1.4.1)
32-
ecosystems-bibliothecary (15.2.0)
32+
ecosystems-bibliothecary (15.3.0)
3333
bundler
3434
csv
3535
json (~> 2.8)
@@ -58,16 +58,16 @@ GEM
5858
pp (0.6.3)
5959
prettyprint
6060
prettyprint (0.2.0)
61-
prism (1.7.0)
61+
prism (1.8.0)
6262
psych (5.3.1)
6363
date
6464
stringio
6565
public_suffix (7.0.2)
66-
purl (1.7.0)
66+
purl (1.7.1)
6767
addressable (~> 2.8)
6868
racc (1.8.1)
6969
rake (13.3.1)
70-
rdoc (7.0.3)
70+
rdoc (7.1.0)
7171
erb
7272
psych (>= 4.0.0)
7373
tsort
@@ -98,7 +98,7 @@ GEM
9898
stringio (3.2.0)
9999
tomlrb (2.0.4)
100100
tsort (0.2.0)
101-
vers (1.0.2)
101+
vers (1.0.3)
102102
webmock (3.26.1)
103103
addressable (>= 2.8.0)
104104
crack (>= 0.3.2)
@@ -136,7 +136,7 @@ CHECKSUMS
136136
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
137137
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
138138
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
139-
ecosystems-bibliothecary (15.2.0) sha256=bef81a0175f8bdf1d61938d5d5d32e226ec4ff44a54d5d5d34faea663ed67a24
139+
ecosystems-bibliothecary (15.3.0) sha256=dc3c8caa3218bf833beba9e3eb8cbeb35987f771532ff0eab87fbcdfb30ce4eb
140140
erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
141141
git-pkgs (0.8.0)
142142
hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
@@ -150,13 +150,13 @@ CHECKSUMS
150150
ox (2.14.23) sha256=4a9aedb4d6c78c5ebac1d7287dc7cc6808e14a8831d7adb727438f6a1b461b66
151151
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
152152
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
153-
prism (1.7.0) sha256=10062f734bf7985c8424c44fac382ac04a58124ea3d220ec3ba9fe4f2da65103
153+
prism (1.8.0) sha256=84453a16ef5530ea62c5f03ec16b52a459575ad4e7b9c2b360fd8ce2c39c1254
154154
psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
155155
public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857
156-
purl (1.7.0) sha256=e25a6b951975e94104a17d8d40e8529fa882a5a63717c68af2390e9b8d0ac3f2
156+
purl (1.7.1) sha256=459aecf3e0e2199bdfafa2885731b39af7c081dcd1d63d7ec036aab732e56ed3
157157
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
158158
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
159-
rdoc (7.0.3) sha256=dfe3d0981d19b7bba71d9dbaeb57c9f4e3a7a4103162148a559c4fc687ea81f9
159+
rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
160160
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
161161
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
162162
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
@@ -180,7 +180,7 @@ CHECKSUMS
180180
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
181181
tomlrb (2.0.4) sha256=262f77947ac3ac9b3366a0a5940ecd238300c553e2e14f22009e2afcd2181b99
182182
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
183-
vers (1.0.2) sha256=0ea9a63acbe1f197268c7da93f0708a4fc99bd88d86aa49dccf5b1b8d4c68de5
183+
vers (1.0.3) sha256=d1ef5b0f146a23515ab94b0a6d7ad7338494f90af358857da83a7d8a9921bc89
184184
webmock (3.26.1) sha256=4f696fb57c90a827c20aadb2d4f9058bbff10f7f043bd0d4c3f58791143b1cd7
185185

186186
BUNDLED WITH

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,19 @@ Output formats: `text` (default), `json`, and `sarif`. SARIF integrates with Git
322322
323323
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.
324324

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+
325338
### Diff between commits
326339

327340
```bash

docs/internals.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The schema has ten tables. Six handle dependency tracking:
1919
- `branch_commits` is a join table preserving commit order within each branch
2020
- `manifests` stores file paths with their ecosystem (npm, rubygems, etc.) and kind (manifest vs lockfile)
2121
- `dependency_changes` records every add, modify, or remove event
22-
- `dependency_snapshots` stores full dependency state at intervals
22+
- `dependency_snapshots` stores full dependency state at intervals, including lockfile integrity hashes
2323

2424
Four more support vulnerability scanning and package enrichment:
2525

@@ -227,6 +227,25 @@ The `licenses` command checks licenses against configured policies:
227227

228228
The command exits with code 1 when violations are found, making it suitable for CI pipelines.
229229

230+
## Integrity Verification
231+
232+
The `integrity` command shows SHA256 hashes from lockfiles. Modern lockfiles include checksums: Gemfile.lock has a CHECKSUMS section, package-lock.json has integrity fields. These hashes verify that the exact package content matches what was originally resolved.
233+
234+
The `dependency_snapshots` table stores integrity alongside other dependency data. During `init` and `update`, bibliothecary extracts the integrity field from parsed lockfiles and git-pkgs stores it with each snapshot.
235+
236+
The `--drift` flag does two things:
237+
238+
1. **Internal drift** - detects when the same package@version has different integrity hashes across your history. This shouldn't happen under normal circumstances and could indicate:
239+
- A dependency was republished with different content (rare but possible on some registries)
240+
- A supply chain attack replaced a package
241+
- Lockfile corruption or manual editing
242+
243+
2. **Registry mismatch** - compares lockfile hashes against the registry's published integrity via the ecosyste.ms API. A mismatch here is more serious: either your lockfile has been tampered with, or the registry itself has different content than what you resolved.
244+
245+
Internal drift queries the database for unique (purl, integrity) pairs and flags purls with multiple different values. Registry comparison fetches each version's integrity from ecosyste.ms (using the purl gem's `ecosystems_version_api_url` method) and compares against the lockfile value.
246+
247+
Unlike the `outdated` and `licenses` commands which are entirely extrinsic, integrity verification is primarily intrinsic (lockfile hashes come from your git history) with optional extrinsic comparison when using `--drift`.
248+
230249
## Models
231250

232251
Sequel models live in [`lib/git/pkgs/models/`](../lib/git/pkgs/models/). They're straightforward except for a few convenience methods:

git-pkgs.gemspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Gem::Specification.new do |spec|
3434
spec.add_dependency "rugged", "~> 1.0"
3535
spec.add_dependency "sequel", ">= 5.0"
3636
spec.add_dependency "sqlite3", ">= 2.0"
37-
spec.add_dependency "ecosystems-bibliothecary", "~> 15.2"
37+
spec.add_dependency "ecosystems-bibliothecary", "~> 15.3"
3838
spec.add_dependency "vers", "~> 1.0"
39-
spec.add_dependency "purl", "~> 1.7"
39+
spec.add_dependency "purl", "~> 1.7", ">= 1.7.1"
4040
spec.add_dependency "sarif-ruby"
4141
end

lib/git/pkgs.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@
4949
require_relative "pkgs/commands/vulns"
5050
require_relative "pkgs/commands/outdated"
5151
require_relative "pkgs/commands/licenses"
52+
require_relative "pkgs/commands/integrity"
5253

5354
module Git
5455
module Pkgs
5556
class Error < StandardError; end
5657
class NotInitializedError < Error; end
5758
class NotInGitRepoError < Error; end
59+
class SchemaVersionError < Error; end
5860

5961
class << self
6062
attr_accessor :quiet, :git_dir, :work_tree, :db_path, :batch_size, :snapshot_interval, :threads

lib/git/pkgs/analyzer.rb

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ def analyze_commit(rugged_commit, previous_snapshot = {})
141141
purl: generate_purl(result[:platform], dep[:name]),
142142
change_type: "added",
143143
requirement: dep[:requirement],
144-
dependency_type: dep[:type]
144+
dependency_type: dep[:type],
145+
integrity: dep[:integrity]
145146
}
146147

147148
key = [manifest_path, dep[:name]]
@@ -150,7 +151,8 @@ def analyze_commit(rugged_commit, previous_snapshot = {})
150151
kind: result[:kind],
151152
purl: generate_purl(result[:platform], dep[:name]),
152153
requirement: dep[:requirement],
153-
dependency_type: dep[:type]
154+
dependency_type: dep[:type],
155+
integrity: dep[:integrity]
154156
}
155157
end
156158
end
@@ -179,7 +181,8 @@ def analyze_commit(rugged_commit, previous_snapshot = {})
179181
purl: generate_purl(after_result[:platform], name),
180182
change_type: "added",
181183
requirement: dep[:requirement],
182-
dependency_type: dep[:type]
184+
dependency_type: dep[:type],
185+
integrity: dep[:integrity]
183186
}
184187

185188
key = [manifest_path, name]
@@ -188,7 +191,8 @@ def analyze_commit(rugged_commit, previous_snapshot = {})
188191
kind: after_result[:kind],
189192
purl: generate_purl(after_result[:platform], name),
190193
requirement: dep[:requirement],
191-
dependency_type: dep[:type]
194+
dependency_type: dep[:type],
195+
integrity: dep[:integrity]
192196
}
193197
end
194198

@@ -202,7 +206,8 @@ def analyze_commit(rugged_commit, previous_snapshot = {})
202206
purl: generate_purl(before_result[:platform], name),
203207
change_type: "removed",
204208
requirement: dep[:requirement],
205-
dependency_type: dep[:type]
209+
dependency_type: dep[:type],
210+
integrity: dep[:integrity]
206211
}
207212

208213
key = [manifest_path, name]
@@ -223,7 +228,8 @@ def analyze_commit(rugged_commit, previous_snapshot = {})
223228
change_type: "modified",
224229
requirement: after_dep[:requirement],
225230
previous_requirement: before_dep[:requirement],
226-
dependency_type: after_dep[:type]
231+
dependency_type: after_dep[:type],
232+
integrity: after_dep[:integrity]
227233
}
228234

229235
key = [manifest_path, name]
@@ -232,7 +238,8 @@ def analyze_commit(rugged_commit, previous_snapshot = {})
232238
kind: after_result[:kind],
233239
purl: generate_purl(after_result[:platform], name),
234240
requirement: after_dep[:requirement],
235-
dependency_type: after_dep[:type]
241+
dependency_type: after_dep[:type],
242+
integrity: after_dep[:integrity]
236243
}
237244
end
238245
end
@@ -252,7 +259,8 @@ def analyze_commit(rugged_commit, previous_snapshot = {})
252259
purl: generate_purl(result[:platform], dep[:name]),
253260
change_type: "removed",
254261
requirement: dep[:requirement],
255-
dependency_type: dep[:type]
262+
dependency_type: dep[:type],
263+
integrity: dep[:integrity]
256264
}
257265

258266
key = [manifest_path, dep[:name]]
@@ -329,7 +337,8 @@ def dependencies_at_commit(rugged_commit)
329337
kind: result[:kind],
330338
purl: generate_purl(result[:platform], dep[:name]),
331339
requirement: dep[:requirement],
332-
dependency_type: dep[:type]
340+
dependency_type: dep[:type],
341+
integrity: dep[:integrity]
333342
}
334343
end
335344
end

lib/git/pkgs/cli.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ class CLI
3535
"stats" => "Show dependency statistics",
3636
"stale" => "Show dependencies that haven't been updated",
3737
"outdated" => "Show packages with newer versions available",
38-
"licenses" => "Show licenses for dependencies"
38+
"licenses" => "Show licenses for dependencies",
39+
"integrity" => "Show and verify lockfile integrity hashes"
3940
},
4041
"Security" => {
4142
"vulns" => "Scan for known vulnerabilities"

lib/git/pkgs/commands/branch.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def bulk_process_commits(commits, branch, analyzer, total, repo)
191191
ecosystem: s[:ecosystem],
192192
requirement: s[:requirement],
193193
dependency_type: s[:dependency_type],
194+
integrity: s[:integrity],
194195
created_at: now,
195196
updated_at: now
196197
}
@@ -266,7 +267,8 @@ def bulk_process_commits(commits, branch, analyzer, total, repo)
266267
name: name,
267268
ecosystem: dep_info[:ecosystem],
268269
requirement: dep_info[:requirement],
269-
dependency_type: dep_info[:dependency_type]
270+
dependency_type: dep_info[:dependency_type],
271+
integrity: dep_info[:integrity]
270272
}
271273
end
272274
snapshots_stored += snapshot.size
@@ -286,7 +288,8 @@ def bulk_process_commits(commits, branch, analyzer, total, repo)
286288
name: name,
287289
ecosystem: dep_info[:ecosystem],
288290
requirement: dep_info[:requirement],
289-
dependency_type: dep_info[:dependency_type]
291+
dependency_type: dep_info[:dependency_type],
292+
integrity: dep_info[:integrity]
290293
}
291294
end
292295
snapshots_stored += snapshot.size

lib/git/pkgs/commands/init.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def bulk_process_commits(commits, branch, analyzer, total)
125125
purl: s[:purl],
126126
requirement: s[:requirement],
127127
dependency_type: s[:dependency_type],
128+
integrity: s[:integrity],
128129
created_at: now,
129130
updated_at: now
130131
}
@@ -207,7 +208,8 @@ def bulk_process_commits(commits, branch, analyzer, total)
207208
ecosystem: dep_info[:ecosystem],
208209
purl: dep_info[:purl],
209210
requirement: dep_info[:requirement],
210-
dependency_type: dep_info[:dependency_type]
211+
dependency_type: dep_info[:dependency_type],
212+
integrity: dep_info[:integrity]
211213
}
212214
end
213215
snapshots_stored += snapshot.size
@@ -228,7 +230,8 @@ def bulk_process_commits(commits, branch, analyzer, total)
228230
ecosystem: dep_info[:ecosystem],
229231
purl: dep_info[:purl],
230232
requirement: dep_info[:requirement],
231-
dependency_type: dep_info[:dependency_type]
233+
dependency_type: dep_info[:dependency_type],
234+
integrity: dep_info[:integrity]
232235
}
233236
end
234237
snapshots_stored += snapshot.size

0 commit comments

Comments
 (0)