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 ¶
- type Candidate
- type Cloner
- type DroppedCandidate
- type GoToolchain
- func (t *GoToolchain) DepGoVersions(ctx context.Context, dir string) (map[string]string, error)
- func (t *GoToolchain) Get(ctx context.Context, dir, moduleAtVersion string) error
- func (t *GoToolchain) Linked(ctx context.Context, dir string, patterns []string) (modules, packages map[string]struct{}, err error)
- func (t *GoToolchain) LinkedStd(ctx context.Context, dir string, patterns []string) (map[string]struct{}, error)
- func (t *GoToolchain) ListModules(ctx context.Context, dir string) (map[string]string, error)
- func (t *GoToolchain) ModTidy(ctx context.Context, dir string) error
- func (t *GoToolchain) Replace(ctx context.Context, dir, oldPath, newPath, version string) error
- func (t *GoToolchain) Replaces(_ context.Context, dir string) (map[string]ReplaceTarget, error)
- func (t *GoToolchain) Requirements(_ context.Context, dir string) (map[string]string, error)
- type ModrootRequest
- type ModrootResult
- type Options
- type ReplaceTarget
- type Residual
- type Scanner
- type Simulator
- type Toolchain
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.
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 ¶
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 (*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 ¶
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 ¶
WithDefaults fills unset options.
type ReplaceTarget ¶
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 ¶
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.