diffx

package
v0.6.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package diffx computes the semantic difference between two lockfiles.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Change

type Change struct {
	Name        string     `json:"name"`
	Ecosystem   string     `json:"ecosystem"`
	Kind        Kind       `json:"kind"`
	Old         []string   `json:"old,omitempty"`
	New         []string   `json:"new,omitempty"`
	Level       vers.Level `json:"-"`
	LevelString string     `json:"level,omitempty"`

	// Why the package is in the tree, when the lockfile records its
	// dependency graph. Origin is "direct", "transitive" or "" (unknown).
	// Via is the chain of dependencies from a direct dependency down to
	// (but excluding) this package, e.g. ["react-scripts", "webpack"].
	Origin string   `json:"origin,omitempty"`
	Via    []string `json:"via,omitempty"`

	// Filled in by the OSV layer.
	IntroducedVulns []Vuln `json:"introduced_vulns,omitempty"` // affect new, not old
	FixedVulns      []Vuln `json:"fixed_vulns,omitempty"`      // affected old, not new
	ExistingVulns   []Vuln `json:"existing_vulns,omitempty"`   // affect both

	// Filled in by the deps.dev layer (registry metadata for the
	// version this change introduces).
	PublishedAt      string `json:"published_at,omitempty"` // RFC3339, UTC
	AgeDays          int    `json:"age_days,omitempty"`
	Fresh            bool   `json:"fresh,omitempty"` // younger than the cooldown window
	Deprecated       bool   `json:"deprecated,omitempty"`
	DeprecatedReason string `json:"deprecated_reason,omitempty"`

	// Unlisted: at least one incoming version is missing from the
	// registry metadata (deps.dev) even though other versions of the
	// same package are listed. That is what an unpublished or deleted
	// release looks like — registries pull malicious versions, so a
	// lockfile that still pins one is a red flag. (A release published
	// minutes ago may also not be indexed yet.)
	Unlisted         bool     `json:"unlisted,omitempty"`
	UnlistedVersions []string `json:"unlisted_versions,omitempty"`

	// ScriptsAdded: the outgoing version ran no install scripts, the
	// incoming one does (npm only; preinstall/install/postinstall, per
	// the registry's hasInstallScript). Adding execution-on-install in
	// an ordinary-looking bump is how several real npm supply-chain
	// attacks shipped their payload. ScriptedVersions lists the
	// incoming versions that carry scripts.
	ScriptsAdded     bool     `json:"install_scripts_added,omitempty"`
	ScriptedVersions []string `json:"scripted_versions,omitempty"`

	// ProvenanceDropped: every known outgoing version of this package
	// was published with sigstore provenance attestations, the incoming
	// one wasn't (npm, PyPI, crates.io). Legitimate CI keeps attesting; a stolen
	// publish token can publish but cannot make the pipeline attest,
	// so a silent drop is worth a look before trusting the release.
	// UnattestedVersions lists the incoming versions without provenance.
	ProvenanceDropped  bool     `json:"provenance_dropped,omitempty"`
	UnattestedVersions []string `json:"unattested_versions,omitempty"`

	// IntegrityChanged: a version pinned on BOTH sides now records a
	// different content hash — the lockfile expects different bytes for
	// the same version string. Registries never change a published
	// artifact, so outside a registry migration this means the tarball
	// was replaced (registry-side tampering, a hijacked mirror, or a
	// hand-edited lockfile). Compared per hash algorithm: a lockfile
	// upgrade that switches sha1 → sha512 is NOT flagged.
	IntegrityChanged  bool     `json:"integrity_changed,omitempty"`
	IntegrityVersions []string `json:"integrity_changed_versions,omitempty"`

	// DigestChanged: a container image tag pinned on both sides now pins
	// a different digest. Unlike registry tarballs, image tags MOVE by
	// design (base images are rebuilt for security fixes), so a digest
	// bump under an unchanged tag is routine — the row is shown
	// neutrally, and the ocireg layer verifies the new digest against
	// the registry instead: DigestVerified means the registry serves
	// exactly this digest for the tag today. A pin the registry does NOT
	// serve for the tag surfaces via TagMismatch; a digest the registry
	// has never seen at all surfaces via Unlisted. Conan recipe
	// revisions ride the same lane ("rrev:" pins): recipes are
	// re-exported for old versions routinely, so a same-version rrev
	// change is shown neutrally as a recipe-revision update.
	DigestChanged  bool   `json:"digest_changed,omitempty"`
	DigestVerified bool   `json:"digest_verified,omitempty"`
	OldDigest      string `json:"old_digest,omitempty"`
	NewDigest      string `json:"new_digest,omitempty"`

	// OldHost/NewHost: the registry hosts the package resolved from on
	// each side, when the lockfile records them and they differ.
	// RegistryMoved marks the dependency-confusion direction: the
	// resolution moved FROM a private/alternate host TO the ecosystem's
	// public registry — exactly what an attacker's higher-versioned
	// public squat of an internal package name looks like. Whole-file
	// mirror migrations (many packages moving between the same two
	// hosts) are recognized and not flagged.
	OldHost       string `json:"old_host,omitempty"`
	NewHost       string `json:"new_host,omitempty"`
	RegistryMoved bool   `json:"registry_moved,omitempty"`

	// ResolvedRefs maps a pinned ref to the release tag it equals, when
	// the upstream repository's tags were checked (GitHub Actions pins: a
	// commit SHA that is exactly the tag vX.Y.Z, or a floating major tag
	// v4 currently pointing at vX.Y.Z). Display and vulnerability
	// matching use the resolved version where available.
	ResolvedRefs map[string]string `json:"resolved_refs,omitempty"`

	// TyposquatOf: this package just entered the tree, its release is
	// young (or of unknown age), and its name is at most one edit away
	// from the named popular package on the same registry — the shape of
	// a typosquatting attack. Set by the squat layer; empty means clean.
	TyposquatOf string `json:"typosquat_of,omitempty"`

	// Channel: the registry channel the package resolves from, when
	// the lockfile records one — the conda channel for conda formats
	// (conda.anaconda.org/<ch>/…), the chart repository URL for Helm.
	// The condareg/helmreg layers key their lookups on it.
	Channel string `json:"-"`

	// NewPins: what the new lockfile itself pins for each incoming
	// version (a content hash, or "commit:<sha>" for git-resolved
	// formats), for layers that verify pins against the outside world.
	// Not serialized — pin-to-pin differences already surface as
	// IntegrityChanged.
	NewPins map[string]string `json:"-"`

	// TagMismatch: the lockfile pins a version at a commit that is NOT
	// what the upstream repository's tag for that version points at
	// today (SwiftPM: Package.resolved records both the version and the
	// resolved commit). Released tags are supposed to be immutable —
	// either the tag has been re-pointed since resolution (how the
	// tj-actions attack shipped), or the lockfile was edited to fetch a
	// different commit while displaying an innocent version.
	TagMismatch   bool     `json:"tag_mismatch,omitempty"`
	TagMismatches []string `json:"tag_mismatches,omitempty"`

	// NonRegistry: the lockfile says this package doesn't come from the
	// public registry (workspace member, path/git dependency). Suppresses
	// the unlisted flag; not serialized.
	NonRegistry bool `json:"-"`

	// License strings as the registry reports them (per side), and
	// whether the bump changes the license. Only set when deps.dev
	// knows both sides.
	OldLicense     string `json:"old_license,omitempty"`
	NewLicense     string `json:"new_license,omitempty"`
	LicenseChanged bool   `json:"license_changed,omitempty"`

	// Filled in by the changelog layer: the upstream repository and, when
	// both versions match real tags there, links that are verified not
	// to 404.
	SourceRepo string `json:"source_repo,omitempty"`
	CompareURL string `json:"compare_url,omitempty"` // upstream diff old → new
	ReleaseURL string `json:"release_url,omitempty"` // release/tag page for new

	// Filled in by the release-notes layer (opt-in via -changelogs):
	// upstream release notes covering the versions this bump pulls in,
	// newest first.
	ReleaseNotes []ReleaseNote `json:"release_notes,omitempty"`

	// Findings acknowledged via a .lockvetignore file. Ignored lists the
	// suppressed finding kinds ("fresh", "major", …); IgnoredVulns holds
	// advisories moved out of IntroducedVulns. Both stay visible in JSON
	// and as a dim marker in reports, but no longer count toward the
	// summary or -fail-on gates.
	Ignored      []string `json:"ignored,omitempty"`
	IgnoredVulns []Vuln   `json:"ignored_vulns,omitempty"`
}

Change describes what happened to one package.

func (*Change) HasIgnored added in v0.4.1

func (c *Change) HasIgnored(kind string) bool

HasIgnored reports whether the finding kind was suppressed on this change via a .lockvetignore rule.

type FileDiff

type FileDiff struct {
	Path      string   `json:"path"`
	Kind      string   `json:"lockfile"`
	Ecosystem string   `json:"ecosystem"`
	Changes   []Change `json:"changes"`
}

FileDiff is the set of changes within one lockfile.

func Diff

func Diff(oldF, newF *lock.File) FileDiff

Diff compares two parsed lockfiles (either may be nil for created/deleted).

func Filter

func Filter(diffs []FileDiff, patterns string) []FileDiff

Filter keeps only the changes whose package name — or any package in their via chain — matches one of the comma-separated glob patterns. Matching is case-insensitive; '*' matches any run of characters (including '/', so "*sys*" matches "golang.org/x/sys") and '?' matches one character. Matching via chains means "-only jiff" also shows every transitive change that jiff dragged in.

func (*FileDiff) Sort added in v0.5.0

func (fd *FileDiff) Sort()

Sort orders changes most-alarming first (major level, then kind, then name). Diff calls it; annotation layers that rewrite levels afterwards (actreg resolving SHA pins to tags) call it again.

type Kind

type Kind string

Kind of change for a single package.

const (
	Added      Kind = "added"
	Removed    Kind = "removed"
	Upgraded   Kind = "upgraded"
	Downgraded Kind = "downgraded"
	Changed    Kind = "changed"  // multi-version set changed
	Repinned   Kind = "repinned" // same version(s), but integrity/resolution changed
)

type ReleaseNote added in v0.3.3

type ReleaseNote struct {
	Tag     string `json:"tag"`
	Title   string `json:"title,omitempty"`
	URL     string `json:"url"`
	Excerpt string `json:"excerpt,omitempty"`
}

ReleaseNote is one upstream release's notes, excerpted.

type Summary

type Summary struct {
	Total, Major, Minor, Patch, Added, Removed, Downgraded int
	VulnsIntroduced, VulnsFixed, VulnsExisting             int
	Fresh, Deprecated, LicenseChanged, Unlisted            int
	ScriptsAdded                                           int // npm bumps that newly run install scripts
	Typosquats                                             int // young additions confusable with a popular package
	ProvenanceDropped                                      int // npm bumps that silently stop attesting provenance
	IntegrityChanged                                       int // pins whose recorded content hash changed for an unchanged version
	TagMismatch                                            int // pins whose commit is not what the upstream tag points at
	RegistryMoved                                          int // resolutions moved from a private host to the public registry
	Direct, Transitive                                     int // 0/0 when the formats record no graph
	Ignored                                                int // findings acknowledged via .lockvetignore
}

Summary aggregates counts across file diffs.

func Summarize

func Summarize(diffs []FileDiff) Summary

Summarize computes totals for a set of file diffs.

type Vuln

type Vuln struct {
	ID       string `json:"id"`
	Summary  string `json:"summary,omitempty"`
	Severity string `json:"severity,omitempty"`
	URL      string `json:"url,omitempty"`
	// FixedIn is the smallest version that clears this advisory for the
	// pinned version(s), read from the advisory's own ranges. Empty when
	// no fix has been released or the ranges don't say.
	FixedIn string `json:"fixed_in,omitempty"`
}

Vuln is a known vulnerability reference.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL