Documentation
¶
Overview ¶
Package fsref resolves a path into the single metadata record every rule and collector reads from.
The whole design rests on one invariant: a path is resolved exactly once per walk step, no matter how many rules examine it. Collectors never call stat themselves; they ask the Cache, which the walker has already primed.
Index ¶
- Constants
- Variables
- func Canonical(p string) string
- func CleanImagePath(p string) (string, error)
- func CreateNoFollow(path string, perm os.FileMode) (*os.File, error)
- func DestinationUnder(dir, rel string) (string, error)
- func DirExistsBeneath(root, rel string) bool
- func ExistsBeneath(root, rel string) bool
- func Join(root, rel string) string
- func OpenBeneath(root, rel string) (*os.File, error)
- func ReadBeneath(root, rel string) ([]byte, error)
- func Rel(real, root string) string
- type Cache
- type FileRef
- func (f *FileRef) Immutable() (set, known bool)
- func (f *FileRef) IsBlockDev() bool
- func (f *FileRef) IsCharDev() bool
- func (f *FileRef) IsDir() bool
- func (f *FileRef) IsFIFO() bool
- func (f *FileRef) IsRegular() bool
- func (f *FileRef) IsSocket() bool
- func (f *FileRef) IsSymlink() bool
- func (f *FileRef) Link() (string, error)
- func (f *FileRef) ModeString() string
- func (f *FileRef) Perm() uint16
- func (f *FileRef) String() string
- func (f *FileRef) TypeChar() byte
Constants ¶
const ( // stx_attributes bits we care about; see statx(2). AttrCompressed = 0x00000004 AttrImmutable = 0x00000010 AttrAppend = 0x00000020 AttrNodump = 0x00000040 AttrEncrypted = 0x00000800 )
const MaxMetadataFile = 4 << 20
MaxMetadataFile caps how much of an image file the metadata helpers will read. Hostnames and passwd files are small; anything larger is not one.
Variables ¶
var ErrEscapesRoot = fmt.Errorf("path escapes the collection root")
ErrEscapesRoot reports a path that would leave the tree it is supposed to stay inside.
Functions ¶
func Canonical ¶
Canonical resolves a path to an absolute, symlink-free form, for comparing two paths that may be written differently.
It is how the walk recognises its own output directory: that path is chosen by the operator, so it may be relative, and it may be reached through a symlink. A path that cannot be resolved -- it does not exist yet, or a component is unreadable -- is returned cleaned, which is the best available answer and never worse than the input.
func CleanImagePath ¶
CleanImagePath normalises a path taken from inside the image.
Paths that come from a file's *contents* -- a HISTFILE assignment in an rc file, say -- are attacker-controlled on a hostile image. Interpreting them requires remembering that "/" means the image root, not the examiner's root, so "/../../etc/shadow" resolves back to "/etc/shadow" inside the image rather than climbing out of it. path.Clean does exactly that: leading ".." at the root are absorbed.
A path that is not absolute is rejected: the shell would have resolved it against a working directory this walk does not have.
func CreateNoFollow ¶
CreateNoFollow creates a file for writing, refusing to follow a symlink that is already sitting at the destination.
Without O_NOFOLLOW, a symlink left in the output tree -- by a previous run, or by anything else with write access to it -- would redirect a collected file somewhere else entirely.
func DestinationUnder ¶
DestinationUnder joins a relative path onto a destination directory and verifies the result stays inside it.
The copy destination is built from the same image-controlled path as the source, so it needs the same containment check. filepath.Join cleans, which means a path that climbed out would do so silently.
func DirExistsBeneath ¶
DirExistsBeneath reports whether rel names an existing directory inside root.
func ExistsBeneath ¶
ExistsBeneath reports whether rel names an existing regular file inside root. Used for the operating-system markers, where a symlink pointing out of the image must not count as evidence of anything.
func OpenBeneath ¶
OpenBeneath opens a regular file inside root for reading, without following symlinks and without blocking.
The metadata helpers -- hostname, account database, OS markers -- read files chosen by the image, and a hostile image can make any of them a symlink to the examiner's filesystem or a FIFO that never opens. os.Open does both: follows the link, and blocks. So the path is contained first, the open is O_NOFOLLOW|O_NONBLOCK, and the result must be a regular file.
func ReadBeneath ¶
ReadBeneath reads a metadata file from inside root, subject to the same containment and file-type rules as OpenBeneath and capped in size.
Types ¶
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is the single-entry memo that keeps InspectFile(path) from turning into one stat per rule. The walker primes it before dispatching a path, so every collector asking for the same path gets the already-resolved record. A miss is not an error -- it just means somebody called a collector outside a walk, so we resolve on demand and stay correct.
type FileRef ¶
type FileRef struct {
Path string // image-relative, e.g. /etc/passwd -- this is what gets recorded
Real string // syscall path, e.g. /mnt/img/etc/passwd
Name string // basename
Depth int // 0 for the walk root
RawMode uint16 // st_mode, type bits included
Nlink uint64
UID uint32
GID uint32
Ino uint64
Dev uint64
Size int64
Atime time.Time
Mtime time.Time
Ctime time.Time
Btime time.Time
HasBtime bool
Attrs uint64 // stx_attributes
AttrsMask uint64 // which bits of Attrs the filesystem actually reports
// contains filtered or unexported fields
}
FileRef is the resolved metadata for one path. It holds no file descriptor: an open descriptor would cost two extra syscalls per file, would pin the inode, and on a mounted image would risk opening the *host's* device nodes. Bytes are handled separately, by the content broker, and only for files some rule actually asked to read.
func Resolve ¶
Resolve stats one path. real is the path to hand the kernel; rel is the image-relative path to record.
func ResolveBeneath ¶
ResolveBeneath stats an image-relative path while guaranteeing the result lies inside root.
Cleaning the path is not enough on its own. The kernel follows symlinks in the intermediate components, so an image containing "/logs -> /" turns "<mount>/logs/etc/shadow" into the *examiner's* /etc/shadow -- and on a scan run as root that is a read of the host, or, once the same path reaches the copy destination, a write outside the output directory.
On Linux the kernel is asked to resolve the path under the root with openat2's RESOLVE_BENEATH, which fails outright if any component would leave it -- through a symlink, a magic link, or "..". That is one atomic resolution rather than a sequence of separate checks.
Where openat2 is unavailable -- an older kernel, or any other platform -- every directory leading to the target is checked with lstat instead, and a symlinked one is refused.
Either way this is a check followed by an open by path, so a window remains against something mutating the image mid-scan. That does not apply to the case it defends against: a forensic image is static, and mounted read-only if the examiner is doing it properly.
func (*FileRef) Immutable ¶
Immutable reports whether the immutable flag is set, and whether the filesystem actually answered. statx gives us this without the FS_IOC_GETFLAGS ioctl, which would have required an open descriptor.
func (*FileRef) IsBlockDev ¶
func (*FileRef) ModeString ¶
ModeString renders permissions the way GNU stat's %A does, which is the format the mactime bodyfile expects.