Documentation
¶
Overview ¶
Package target models the two things vexscan can analyze — an extracted container image and a source checkout — behind a shared vocabulary the ecosystem plugins consume.
Index ¶
- Constants
- func IsKernelFS(name string) bool
- func Rel(root, hostPath string) string
- func Resolve(root, name string) (string, error)
- func ResolveParent(root, name string) (string, error)
- type DirFS
- func (d *DirFS) HostPath(name string) (string, error)
- func (d *DirFS) LinkTarget(name string) (string, error)
- func (d *DirFS) Lstat(name string) (fs.FileInfo, error)
- func (d *DirFS) Open(name string) (io.ReadCloser, error)
- func (d *DirFS) ReadDir(name string) ([]fs.DirEntry, error)
- func (d *DirFS) ReadFile(name string) ([]byte, error)
- func (d *DirFS) Root() string
- func (d *DirFS) Stat(name string) (fs.FileInfo, error)
- func (d *DirFS) Unreadable() Unreadable
- func (d *DirFS) Walk(name string, fn WalkFunc) error
- type Image
- type ImageConfig
- type RootFS
- type Source
- type Unreadable
- type WalkFunc
Constants ¶
const MaxSymlinkHops = 64
MaxSymlinkHops bounds symlink resolution so a tree containing a link cycle cannot hang a caller.
Variables ¶
This section is empty.
Functions ¶
func IsKernelFS ¶
IsKernelFS reports whether a tree-absolute directory is a pseudo-filesystem mount point that a walk should not descend into.
An extracted image contains none of these, but a rootfs captured from a running system does, and they ship no code: /proc alone is tens of thousands of synthetic entries that stat as regular files. Walk itself does not apply this -- it is a question about what is worth looking at, not about what the tree contains -- so each walker asks.
func Rel ¶
Rel converts a host path under root back to the tree-absolute path it has from inside. A path outside root is returned unchanged.
func Resolve ¶
Resolve maps a tree-absolute path to a host path inside root, following symlinks that exist in the tree but never escaping it. Absolute link targets are re-rooted, exactly as they would resolve from inside a container, and ".." is clamped at root rather than allowed to walk out. Resolution is lexical (no os.Root) so the module keeps its go 1.23 directive.
func ResolveParent ¶
ResolveParent resolves everything but the last component of name, leaving a final symlink unfollowed. Writers use it so that creating an entry over an existing symlink replaces the link instead of writing through it; readers use it for lstat and readlink.
Types ¶
type DirFS ¶
type DirFS struct {
// contains filtered or unexported fields
}
DirFS is a RootFS backed by a host directory: an extracted image, or a rootfs the user already had on disk.
func (*DirFS) Unreadable ¶
func (d *DirFS) Unreadable() Unreadable
type Image ¶
type Image struct {
// Ref is the target as the user named it: an image reference, or the
// directory a rootfs was read from.
Ref string
// OS and Arch are the platform variant that was pulled. Both are empty for
// a rootfs, which was never pulled and whose platform nobody declared.
OS string
Arch string
// Config is how the image says it is meant to be run. It is the zero value
// for a rootfs: a directory carries no entrypoint, no env and no PATH.
Config ImageConfig
FS RootFS
}
Image is an extracted container image: its filesystem plus the configuration that says how it is meant to be run.
It also carries a rootfs the user already had on disk, which is the same thing minus the parts only a registry can supply. Nothing here is required: no analyzer reads Ref, OS or Arch, and Config is optional by construction -- a plugin that wants an entrypoint and finds none taints its conclusions rather than failing. So a tree with nothing but FS set is a scannable target, just one that can say less.
type ImageConfig ¶
type ImageConfig struct {
Entrypoint []string `json:"entrypoint,omitempty"`
Cmd []string `json:"cmd,omitempty"`
Env []string `json:"env,omitempty"`
WorkingDir string `json:"working_dir,omitempty"`
User string `json:"user,omitempty"`
}
ImageConfig is the subset of the OCI image configuration vexscan reasons about. Entrypoint and Cmd matter most: they are what roots the ELF reachability closure. Without them there is nothing to walk the shared-library graph from, and every library in the image has to be treated as potentially loaded.
func (ImageConfig) Argv ¶
func (c ImageConfig) Argv() []string
Argv is the command the image runs by default: Entrypoint with Cmd appended, matching how a runtime composes them when Entrypoint is set.
func (ImageConfig) LookupEnv ¶
func (c ImageConfig) LookupEnv(key string) (string, bool)
LookupEnv returns the value of key in the image environment. Later entries win, as they do when a runtime builds the environment.
func (ImageConfig) PathDirs ¶
func (c ImageConfig) PathDirs() []string
PathDirs is the image's PATH, split, falling back to the runtime default when the config sets none. Used to resolve a bare argv[0] to an actual file.
type RootFS ¶
type RootFS interface {
// Root reports the host directory the tree lives in.
Root() string
// HostPath maps a tree-absolute path to a host path, following symlinks.
// The result is guaranteed to be inside Root.
HostPath(name string) (string, error)
// Open opens a file for reading, following symlinks.
Open(name string) (io.ReadCloser, error)
// ReadFile reads a whole file, following symlinks.
ReadFile(name string) ([]byte, error)
// Stat follows symlinks; Lstat reports on the link itself.
Stat(name string) (fs.FileInfo, error)
Lstat(name string) (fs.FileInfo, error)
// LinkTarget returns the raw, unresolved text of the symlink at name, or
// an error when name is not a symlink.
LinkTarget(name string) (string, error)
// ReadDir lists a directory in lexical order.
ReadDir(name string) ([]fs.DirEntry, error)
// Walk visits name and everything beneath it, without following symlinks
// (so a link cycle cannot make it loop).
Walk(name string, fn WalkFunc) error
// Unreadable reports what Walk skipped, accumulated across every walk of
// this tree and deduplicated by path. It is cumulative rather than
// per-walk because the question it answers -- was any of this tree
// invisible to the scan -- is a property of the tree, and several plugins
// walk the same one.
Unreadable() Unreadable
}
RootFS is a read-only view of a filesystem tree that lives in a directory on the host but is addressed by the absolute paths it would have from inside.
This is deliberately not an fs.FS. Three consumers need exactly what fs.FS refuses to provide: HostPath, because govulncheck is an exec'd subprocess that needs a real path; LinkTarget, because shared-library soname resolution is mostly symlink chasing and needs the raw link text; and absolute paths, because everything inside a container image refers to "/usr/lib", not a slash-free relative name.
Every method takes a tree-absolute path. Implementations must confine resolution to the tree: a symlink pointing at /etc resolves to /etc inside the tree, never to the host's /etc.
type Source ¶
type Source struct {
// Ref is the repo as the user named it: a URL, owner/repo, or local path.
Ref string
// Rev is the resolved branch, tag or commit, when known.
Rev string
// Dir is the host directory of the module to analyze — the checkout root
// joined with any requested subdirectory.
Dir string
// Subdir is that subdirectory relative to the checkout root ("." at the
// top level).
Subdir string
FS RootFS
}
Source is a checked-out source tree.
Unlike an image it has no meaningful "inside" path space — analysis tools run against host paths — so Dir, not FS, is what most consumers want. FS exists so that plugins which merely look for manifest files (go.mod, package-lock.json, requirements.txt) can share one code path with image mode.
type Unreadable ¶
type Unreadable struct {
// Count is every distinct path skipped, across every walk of the tree.
Count int `json:"count"`
// Paths names the first few, in the order they were encountered. It is
// deliberately not all of them: see maxUnreadableSample.
Paths []string `json:"paths,omitempty"`
}
Unreadable accounts for what a tree's walks could not read.
Walk skips what it cannot read rather than aborting, because one permission-denied entry must not cost the whole scan. This is what keeps that tolerance honest: a subtree that was never looked at is otherwise indistinguishable from a subtree with nothing wrong in it, which is the worst way this tool can be wrong.
Every gap recorded here is an unknown-size one. A walk lists directories and never opens the files in them, so the failure it surfaces is always "this directory would not list" -- which hides an unknown number of unnamed entries, and leaves nothing downstream able to say even what question went unanswered. That is why any entry at all is enough to stop the scan reading as an account of the tree, and why an unreadable *file* does not appear here: it surfaces at the reader that wanted it, which can name what it lost.
func (Unreadable) Any ¶
func (u Unreadable) Any() bool
Any reports whether anything was skipped, and so whether the scan is an incomplete account of the tree.