simulate

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package simulate validates a proposed set of Go module bumps by doing what the melange build's go/bump step will do, ahead of time: clone the upstream source at the exact tag, apply the bumps with the real go toolchain, then OSV-rescan the resolved module graph and raise versions until a fixpoint. The deps list that survives is proven to (a) resolve cleanly and (b) leave no known-fixable vulnerability behind; anything unreachable is reported as a residual rather than silently dropped.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Candidate

type Candidate struct {
	Module  string
	Version string
	FromCVE bool
	VulnIDs []string

	// Replace marks a candidate applied as a go.mod replace directive
	// (survives `go mod tidy`, unlike a plain require pin) rather than a
	// `go get`. ReplaceOld is the directive's left-hand side; empty means a
	// self-replace (ReplaceOld == Module). User-authored YAML replaces enter
	// as replace seeds; the loop also promotes tidy-unsustainable CVE pins.
	Replace    bool
	ReplaceOld string
}

Candidate is one proposed module@version bump. FromCVE marks candidates backed by an OSV advisory: they are defended (retried at @latest, reported as residuals when unreachable), whereas coherence-only candidates (carried-forward pins) are the first to be sacrificed when the module graph won't resolve.

func (Candidate) OldPath

func (c Candidate) OldPath() string

OldPath returns the replace directive's left-hand side, defaulting to the module itself (self-replace).

type Cloner

type Cloner interface {
	CloneAtTag(ctx context.Context, repoURL, tag, destDir string) error
	HeadCommit(dir string) (string, error)
}

Cloner is the source-checkout seam; *git.Client satisfies it.

type DroppedCandidate

type DroppedCandidate struct {
	Module  string `json:"module" yaml:"module"`
	Version string `json:"version" yaml:"version"`
	Reason  string `json:"reason" yaml:"reason"`
}

DroppedCandidate records a candidate removed during simulation and why.

type GoToolchain

type GoToolchain struct {
	// contains filtered or unexported fields
}

GoToolchain runs the local go binary. The environment passes through the user's proxy/auth configuration (GOPROXY, GOPRIVATE, netrc via HOME) and forces the settings resolution parity depends on: GOTOOLCHAIN=auto so a go.mod demanding a newer toolchain is honored, -mod=mod so a vendor/ directory doesn't mask resolution, and GOWORK=off so a stray go.work can't leak into the analysis.

func NewToolchain

func NewToolchain(commandTimeout time.Duration) (*GoToolchain, error)

NewToolchain locates the go binary; the error return lets callers degrade gracefully (skip validation, report it) when no toolchain is available.

func (*GoToolchain) DepGoVersions

func (t *GoToolchain) DepGoVersions(ctx context.Context, dir string) (map[string]string, error)

DepGoVersions returns the go directive of every non-main module in the build list (bare form, e.g. "1.24" or "1.24.5"), keyed by module path, with replace directives applied - mirrors ListModules' decode loop and skip-main/skip-local-replacement semantics exactly, substituting GoVersion for Version throughout.

func (*GoToolchain) Get

func (t *GoToolchain) Get(ctx context.Context, dir, moduleAtVersion string) error

func (*GoToolchain) Linked

func (t *GoToolchain) Linked(ctx context.Context, dir string, patterns []string) (modules, packages map[string]struct{}, err error)

Linked returns the modules AND packages in the transitive non-test import graph of the given build patterns - exactly what the linker records in the binary's buildinfo (what APK scanners read), plus the package-level detail needed to check an advisory's vulnerable import paths (a module can be linked via one package while the vulnerable package is not - e.g. x/sys/unix linked, x/sys/windows not). Both sets come from ONE go list walk. Deliberate choices:

  • Test-only imports are excluded BY DESIGN (no -test flag): they never ship in the artifact, which is precisely the narrowing this exists for.
  • No -e flag: a package-load error fails the whole call so the caller fails OPEN (no filtering); -e would silently under-report the import graph and wrongly drop real fixes - the one unacceptable failure mode.
  • Replace directives resolve to the replacement path, matching ListModules' identity space (scan findings name the replacement).
  • GOOS=linux for build-target parity (melange targets linux; GOOS is the axis that flips import sets via _linux.go files). GOARCH stays host, and melange's build tags (netgo, osusergo, user tags) are not replicated - an over-approximation, acceptable.

func (*GoToolchain) LinkedStd

func (t *GoToolchain) LinkedStd(ctx context.Context, dir string, patterns []string) (map[string]struct{}, error)

LinkedStd returns the set of standard-library import paths in the transitive non-test import graph of the given build patterns, evaluated for GOOS=linux (same walk semantics as Linked, inverted filter: standard library packages instead of non-standard ones).

func (*GoToolchain) ListModules

func (t *GoToolchain) ListModules(ctx context.Context, dir string) (map[string]string, error)

func (*GoToolchain) ModTidy

func (t *GoToolchain) ModTidy(ctx context.Context, dir string) error

func (*GoToolchain) Replace

func (t *GoToolchain) Replace(ctx context.Context, dir, oldPath, newPath, version string) error

Replace applies a replace directive with gobump parity: dropreplace first (so a stale directive can't shadow the new one), then the replacement.

func (*GoToolchain) Replaces

func (t *GoToolchain) Replaces(_ context.Context, dir string) (map[string]ReplaceTarget, error)

Replaces parses dir's go.mod replace directives, keyed by the replaced (old) module path. Local filesystem targets carry an empty Version. Note: modfile.ParseLax skips replace directives entirely, so this must use the full parser.

func (*GoToolchain) Requirements

func (t *GoToolchain) Requirements(_ context.Context, dir string) (map[string]string, error)

Requirements parses dir's go.mod require entries. No go tool invocation - the file as tidied IS the contract gobump checks deps entries against.

type ModrootRequest

type ModrootRequest struct {
	Modroot string
	Seeds   []Candidate
	// Baseline maps module path -> effective version in the original
	// manifest (replace directives applied). FinalDeps entries that don't
	// move a module beyond its baseline are dropped as no-ops.
	Baseline map[string]string
	// Packages are the build package patterns (relative to Modroot,
	// go/build's space-separated with.packages grammar) whose non-test
	// import graphs define artifact reachability; empty falls back to ./...
	// (over-approximate, never narrower than the artifact).
	Packages []string
	// VulnImports maps each seed advisory ID to the import paths its
	// vulnerable code lives in (from the analysis scan's OSV metadata; see
	// scan.Vulnerability.VulnerableImports). Seed candidates whose
	// advisories all live in unlinked packages are dropped before the
	// first apply. Absent/empty entries fail open.
	VulnImports map[string][]string
	// BaselineVulnIDs are the advisory IDs the analysis scan found in the
	// pristine graph. Residual advisories with no ID in this set were
	// INTRODUCED by the bump (they apply only to a bumped-to version) and
	// are reported and counted separately. Nil disables introduced-vuln
	// classification (fail open).
	BaselineVulnIDs map[string]struct{}
}

ModrootRequest describes one module root to simulate.

type ModrootResult

type ModrootResult struct {
	Modroot string `json:"modroot" yaml:"modroot"`

	// FinalDeps are the module@version entries proven to apply cleanly, in
	// stable (indirect -> direct -> new, alphabetical within group) order,
	// filtered to entries that actually move the module graph forward
	// relative to the original manifest.
	FinalDeps []string `json:"final_deps" yaml:"final_deps"`

	// FinalReplaces are the replace directives ("old=new@version", gobump
	// grammar) proven to apply cleanly: user-authored seeds plus pins the
	// loop promoted because go mod tidy would not sustain them as plain
	// requires. Note a replace pins its module exactly - future graph
	// demands for a higher version are overridden until a later choam run
	// re-raises the directive.
	FinalReplaces []string `json:"final_replaces,omitempty" yaml:"final_replaces,omitempty"`

	// Resolved is the full module graph (module -> version) after the final
	// clean apply.
	Resolved map[string]string `json:"-" yaml:"-"`

	// Linked is the artifact-linked module set of the final clean state
	// (see Toolchain.Linked); nil when reachability was unavailable and
	// the loop failed open (everything treated as linked). Callers use it
	// to tell which analysis-time findings never affected the artifact.
	Linked map[string]struct{} `json:"-" yaml:"-"`

	// LinkedPackages is the package-level companion of Linked: the import
	// paths in the artifact's non-test import graph, for checking an
	// advisory's vulnerable import paths (nil when unavailable - fail open).
	LinkedPackages map[string]struct{} `json:"-" yaml:"-"`

	// Requires are the final tidied go.mod require entries (module ->
	// version) - the sustainability contract melange's gobump verifies
	// deps entries against. A pin at (or below) its module's Requires
	// version is proven; anything above is not.
	Requires map[string]string `json:"-" yaml:"-"`

	// UnrequiredModules is the degraded module-level reachability signal,
	// populated ONLY when Linked is nil (the go list -deps walk failed open)
	// and the tidied go.mod's go directive is >= 1.17: modules present in
	// Resolved but absent from the tidied require block under any replace
	// identity. Such modules provide no package in the main module's import
	// closure and cannot be linked into any artifact. nil when unavailable or
	// when Linked is authoritative. Module-granular: unlike Linked, membership
	// of the COMPLEMENT does not imply linked (test-only deps are required but
	// never ship).
	UnrequiredModules map[string]struct{} `json:"-" yaml:"-"`

	// MaxDepGoVersion is the highest go directive across the final resolved
	// build list's non-main modules (bare form, e.g. "1.25"); empty when
	// unavailable. The scratch main module's own directive is deliberately
	// not consulted: the parity tidy rewrites it with -go=<host>.
	MaxDepGoVersion string `json:"max_dep_go_version,omitempty" yaml:"max_dep_go_version,omitempty"`

	// StdPackages is the stdlib slice of the final artifact import graph
	// (LinkedStd over the converged checkout); nil when unknown.
	StdPackages map[string]struct{} `json:"-" yaml:"-"`

	// CVEBackedModules lists the modules in FinalDeps whose entries address
	// at least one advisory (vs coherence-only pins) - the simulation-time
	// equivalent of the checker's SecurityBumpModules.
	CVEBackedModules []string `json:"cve_backed_modules,omitempty" yaml:"cve_backed_modules,omitempty"`

	Residuals []Residual         `json:"residuals,omitempty" yaml:"residuals,omitempty"`
	Dropped   []DroppedCandidate `json:"dropped,omitempty" yaml:"dropped,omitempty"`

	// RemainingVulnIDs are the advisory IDs still present in the final
	// resolved graph (the union of all residuals' VulnIDs).
	RemainingVulnIDs []string `json:"remaining_vuln_ids,omitempty" yaml:"remaining_vuln_ids,omitempty"`

	Iterations int  `json:"iterations" yaml:"iterations"`
	Converged  bool `json:"converged" yaml:"converged"`
}

ModrootResult is the outcome of simulating one module root.

func RunLoop

func RunLoop(ctx context.Context, tc Toolchain, sc Scanner, dir string, req ModrootRequest, opts Options) (*ModrootResult, error)

RunLoop applies the seed candidates to the module at dir with the real go toolchain, rescans the resolved graph, raises versions for any residual advisories, and iterates to a fixpoint (see package doc). It returns a ModrootResult even on non-convergence (Converged=false); an error return means the simulation itself could not run (unreadable module, toolchain or scanner failure) and the caller should fall back to unvalidated behavior.

type Options

type Options struct {
	// MaxIterations caps the raise-and-rescan fixpoint loop (default 5).
	MaxIterations int
	// CommandTimeout bounds each go tool invocation (default 2m).
	CommandTimeout time.Duration
	// Budget bounds the whole per-package simulation (default 10m).
	Budget time.Duration
}

Options tunes the simulation.

func (Options) WithDefaults

func (o Options) WithDefaults() Options

WithDefaults fills unset options.

type ReplaceTarget

type ReplaceTarget struct {
	Path    string
	Version string // empty for local filesystem replacements
}

ReplaceTarget is the right-hand side of a go.mod replace directive.

type Residual

type Residual struct {
	Module          string   `json:"module" yaml:"module"`
	ResolvedVersion string   `json:"resolved_version" yaml:"resolved_version"`
	FixedVersion    string   `json:"fixed_version,omitempty" yaml:"fixed_version,omitempty"` // empty: no released fix
	VulnIDs         []string `json:"vuln_ids,omitempty" yaml:"vuln_ids,omitempty"`
	Reason          string   `json:"reason" yaml:"reason"`

	// Introduced marks an advisory absent from the baseline analysis scan -
	// it applies only to a version the bump itself moved to, not to the
	// original graph. Accounting must not subtract introduced residuals from
	// the baseline "found" count.
	Introduced bool `json:"introduced,omitempty" yaml:"introduced,omitempty"`
}

Residual is a vulnerability the simulation could not eliminate, with the reason zero-vulnerability state is unreachable for it.

type Scanner

type Scanner interface {
	ScanPackages(ctx context.Context, pkgs []scan.Package) (*scan.ScanResult, error)
}

Scanner is the vulnerability-scanning seam; *scan.VulnerabilityScanner satisfies it.

type Simulator

type Simulator struct {
	// contains filtered or unexported fields
}

Simulator validates per-modroot bump candidate sets against a real checkout of the upstream source (see package doc).

func NewSimulator

func NewSimulator(cloner Cloner, tc Toolchain, sc Scanner, tempDir string, opts Options) *Simulator

NewSimulator wires a Simulator from its dependencies. tempDir may be empty.

func (*Simulator) Simulate

func (s *Simulator) Simulate(ctx context.Context, repoURL, tag, expectedCommit string, reqs []ModrootRequest) (map[string]*ModrootResult, error)

Simulate clones repoURL at tag once and runs the fixpoint loop for each requested modroot against that checkout. A per-modroot failure is recorded on its result slot as an error return; a clone failure fails the whole simulation. expectedCommit, when non-empty, is verified warn-only (melange enforces it at build time).

type Toolchain

type Toolchain interface {
	// ModTidy runs `go mod tidy` in dir.
	ModTidy(ctx context.Context, dir string) error
	// Get runs `go get module@version` in dir.
	Get(ctx context.Context, dir, moduleAtVersion string) error
	// ListModules returns the resolved module graph (module path -> version)
	// from `go list -m all` in dir, with replace directives applied and the
	// main module and local-path replacements omitted.
	ListModules(ctx context.Context, dir string) (map[string]string, error)
	// Requirements returns the require entries (module path -> version) of
	// dir's go.mod as written - the set melange's gobump verifies deps
	// entries against after its final tidy.
	Requirements(ctx context.Context, dir string) (map[string]string, error)
	// DepGoVersions returns the go directive of every non-main module in the
	// build list (bare form, e.g. "1.24" or "1.24.5"), keyed by module path,
	// with replace directives applied (same skip-main semantics as
	// ListModules).
	DepGoVersions(ctx context.Context, dir string) (map[string]string, error)
	// Replace applies a replace directive (gobump parity:
	// `go mod edit -dropreplace=<old>` then `-replace=<old>=<new>@<version>`).
	Replace(ctx context.Context, dir, oldPath, newPath, version string) error
	// Replaces returns dir's go.mod replace directives keyed by Old.Path; a
	// local filesystem target is reported with Version == "".
	Replaces(ctx context.Context, dir string) (map[string]ReplaceTarget, error)
	// Linked returns the module paths AND package import paths in the
	// transitive non-test import graph of the given build patterns (empty
	// means ./...) - the modules the linker records in the binary's
	// buildinfo, i.e. what actually ships, plus the package-level detail
	// for checking advisories' vulnerable import paths. Errors make
	// reachability filtering fail OPEN (treat everything as linked).
	Linked(ctx context.Context, dir string, patterns []string) (modules, packages map[string]struct{}, err error)
	// LinkedStd returns the set of standard-library import paths in the
	// transitive non-test import graph of the given build patterns (empty
	// means ./...), evaluated for GOOS=linux (same walk semantics as
	// Linked, inverted filter).
	LinkedStd(ctx context.Context, dir string, patterns []string) (map[string]struct{}, error)
}

Toolchain abstracts the go tool operations the simulation needs. The real implementation shells out to the local go binary (see NewToolchain); tests inject fakes.

Jump to

Keyboard shortcuts

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