feat(gate): add a lockfile-tamper-risk check - #2676
Merged
Merged
Conversation
Contributor supply-chain attacks can hand-edit a package-lock.json entry's resolved/integrity value (or point it at a non-registry host) without touching the corresponding package.json version, which the existing dependency-diff/OSV.dev analyzer never sees since it only looks for KNOWN-CVE versions, not resolved-URL/integrity tampering. Adds a deterministic lockfile_tamper_risk finding (src/review/lockfile-tamper.ts) wired through the same config-as-code chain as sizeGateMode/premergeContentRecheck (gate.lockfileIntegrity: off|advisory|block, off by default, config-as-code only — no DB column or dashboard toggle, matching sizeGateMode's footprint) so a repo that has not opted in sees zero behavior change. Closes #2563
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 8d2b409 | Commit Preview URL Branch Preview URL |
Jul 03 2026, 04:09 AM |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2676 +/- ##
==========================================
+ Coverage 96.10% 96.11% +0.01%
==========================================
Files 237 238 +1
Lines 26538 26638 +100
Branches 9624 9663 +39
==========================================
+ Hits 25505 25604 +99
Misses 424 424
- Partials 609 610 +1
🚀 New features to boost your workflow:
|
2 tasks
JSONbored
added a commit
that referenced
this pull request
Jul 3, 2026
…links Fixes two confirmed false-positive bugs in the already-merged #2676 (issue #2563): packagesWithManifestVersionChange only saw package.json's DIRECT dependency blocks, so a transitive dependency's resolved/ integrity bump -- the vast majority of any real lockfile diff -- was always misclassified as tamper risk. Replaced it with a signal comparing a lockfile entry's resolved/integrity change against that SAME entry's own "version" line within the same diff, which a genuine install/update always bumps together for both direct and transitive entries. Also exempts non-http(s) resolved values (npm workspace-local relative paths, e.g. "packages/gittensory-mcp") from the off-registry check -- they were never resolved from a registry, so they can't be off-registry. Addresses gate-review findings on #2676.
JSONbored
added a commit
that referenced
this pull request
Jul 3, 2026
…links (#2692) * fix(review): stop false-positiving on transitive bumps and workspace links Fixes two confirmed false-positive bugs in the already-merged #2676 (issue #2563): packagesWithManifestVersionChange only saw package.json's DIRECT dependency blocks, so a transitive dependency's resolved/ integrity bump -- the vast majority of any real lockfile diff -- was always misclassified as tamper risk. Replaced it with a signal comparing a lockfile entry's resolved/integrity change against that SAME entry's own "version" line within the same diff, which a genuine install/update always bumps together for both direct and transitive entries. Also exempts non-http(s) resolved values (npm workspace-local relative paths, e.g. "packages/gittensory-mcp") from the off-registry check -- they were never resolved from a registry, so they can't be off-registry. Addresses gate-review findings on #2676. * fix(review): drop dead both-undefined guard in lockfile versionChanged removed !== added already evaluates to false when both are undefined, so the guard was unreachable dead code (codecov/patch flagged it). * fix(review): key lockfile-tamper candidates by full entry path, not bare name Two distinct package-lock.json entries can share the same bare package name at different node_modules nesting depths (npm nests a second copy when dependents need incompatible versions). Keying candidates by bare name merged those entries into one shared record, so a legitimate version bump on one entry could mask an unbumped, tampered resolved/integrity edit on the other. The full node_modules/... path is unique per entry and is now used as the map key instead. Addresses gate-review findings on #2692.
This was referenced Jul 3, 2026
Closed
2 tasks
2 tasks
18 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
lockfile_tamper_riskfinding (src/review/lockfile-tamper.ts) that flags achanged
package-lock.jsondiff hunk where aresolved/integrityvalue changed WITHOUT thecorresponding
package.jsondependency version changing, or where aresolvedURL points outsideregistry.npmjs.org— the classic supply-chain hand-edit tell. Matches anypackage-lock.jsonpathdepth (root,
review-enrichment/,apps/gittensory-ui/, or a future workspace) rather than ahardcoded path list.
(
review-enrichment/src/analyzers/lockfile-drift.ts) — that flags KNOWN-CVE versions in changeddeps; this flags lockfile-tamper/resolved-URL integrity, a different threat model, regardless of
whether the substituted version has a published CVE.
gate.lockfileIntegrity: off|advisory|blockgate dimension through the fullconfig-as-code chain, following the exact same pattern as the existing
sizeGateMode/premergeContentRecheckfields (src/types.ts,src/signals/focus-manifest.ts,src/rules/advisory.ts'sisConfiguredGateBlocker,.gittensory.yml.example, andsrc/openapi/schemas.ts'sRepositorySettingsSchema): config-as-code only (no DB column ordashboard toggle), off by default, so a repo that has not opted in sees zero behavior change.
src/queue/processors.ts(maybeAddLockfileTamperFinding,mirroring
maybeAddSecretLeakFinding's structure) — the scan is skipped entirely unlesslockfileIntegrityGateModeis notoff, and any load error is fail-safe swallowed so it can neverdestabilize the gate.
Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally;codecov/patchrequires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate. —src/review/lockfile-tamper.tsis 100% lines/branches/functions (91/91 lines, 85/85 branches per lcov); the touched lines insrc/rules/advisory.ts,src/signals/focus-manifest.ts, andsrc/queue/processors.tsare also fully covered (verified directly againstcoverage/lcov.info).npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderate— 0 vulnerabilitiestest/unit/lockfile-tamper.test.ts(19 tests: legitimate bump does not trigger, hand-edited resolved/integrity with no version bump triggers, off-registryresolvedURL triggers, scoped packages, multi-package title capping, etc.),test/unit/lockfile-tamper-wiring.test.ts(6 tests: off/unset/advisory/block mode gating, files reuse vs. lazy-load, fail-safe error swallowing), plus newgate.lockfileIntegritycoverage intest/unit/focus-manifest.test.ts(parse/round-trip/resolve/warn) andtest/unit/gate-check-policy.test.ts(blocker escalation underblock, advisory-only underadvisory/off).Additionally ran (not in the template list but part of the full local gate):
npm run db:migrations:check(100 migrations OK, unaffected — no migration in this PR),npm run selfhost:env-reference:check,npm run selfhost:validate-observability,npm run cf-typegen:check,npm run ui:openapi:settings-parity,npm run ui:version-audit,npm run ui:test,npm run build:miner,npm run rees:test— all green.If any required check was skipped, explain why:
maincheckout with zero changes (reproduced bygit stashbefore re-running):test/unit/check-migrations-script.test.ts(a worktree/environment-specific subprocess-spawn issue,unrelated to migrations content — the real
db:migrations:checkpasses cleanly) andtest/unit/github-graphql-cache.test.ts's single-flight timing test (passes in isolation, flakes onlyunder full-suite parallel load). Neither touches any file this PR changes.
Safety
RepositorySettingsSchemagainedlockfileIntegrityGateMode;npm run ui:openapiregenerated and committed;ui:openapi:settings-paritypasses.UI Evidencesection below with JPG/JPEG or PNG screenshots arranged as organized, captioned, clickable thumbnails. — N/A, no visible UI changes (backend gate feature only)..gittensory.yml.exampledocuments the newgate.lockfileIntegrityfield;CHANGELOG.mduntouched.UI Evidence
N/A — this PR has no visible UI change (a new backend gate dimension + deterministic finding only).
Notes
src/rules/predicted-gate.ts) out of scope: it neverreceives PR diff content (no source upload), and the existing
secret_leakfinding — the closestanalogue, also a diff-content-based deterministic finding — is likewise absent from that predictor for
the same reason. Matching that established precedent.
maintainerSettingsSchemainsrc/api/routes.ts)and the settings-preview surface (
src/signals/settings-preview.ts) out of scope: this field followsthe
sizeGateMode/premergeContentRecheckprecedent of being config-as-code only (no DB column, nodashboard toggle) — those two existing fields are likewise absent from both surfaces.
package-lock.jsonformat is parsed today (matching the issue's named paths); otherlockfile formats (
yarn.lock,pnpm-lock.yaml, etc.) are not in scope for this check.Closes #2563