rules

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package rules compiles UAC artifact entries into predicates evaluated during a single filesystem walk.

UAC runs one find(1) per artifact entry. This package turns each entry into a Rule whose Match method answers the same question find would have answered, so ~490 separate traversals collapse into one.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsOffline

func IsOffline(collector string) bool

IsOffline reports whether a collector name can run against a mounted image. The command collector cannot: it executes on the live system.

func ResolveHistfile

func ResolveHistfile(value, home string) (string, bool)

ResolveHistfile turns an extracted value into an absolute image path.

UAC's sed rewrites a leading "~/" to the user home and leaves everything else alone, so a value that is neither absolute nor tilde-prefixed would reach cp as a relative path and fail. Rather than reproduce that failure, such values are reported as unresolvable and recorded, which at least leaves a trace that something was found and not collected.

The value comes from inside the image and is therefore attacker-controlled on a hostile one. It is normalised before being returned, so that "/../../etc/ shadow" means /etc/shadow inside the image rather than climbing out of it. Containment of the eventual filesystem access is enforced separately, by fsref.ResolveBeneath, because cleaning cannot see intermediate symlinks.

Types

type Env

type Env struct {
	MountPoint string
	Now        time.Time

	// OS is the operating system of the image being collected from, used to
	// skip artifacts that declare a supported_os not covering it. Unknown
	// disables the filter rather than dropping everything.
	OS targetos.OS

	// Date range, in days before Now. Zero disables, matching UAC.
	StartDateDays int
	EndDateDays   int

	// Which timestamps the date range tests. UAC's shipped config enables
	// mtime and ctime but *not* atime, so a file touched only by a read stays
	// out of range. Defaults here match that; see internal/config.
	EnableMtime bool
	EnableAtime bool
	EnableCtime bool

	// HashAlgorithm is the digest set the hash collector produces. UAC
	// defaults to md5 and sha1.
	HashAlgorithm []string

	// ExcludeNamePattern and MaxDepth come from uac.conf and apply to every
	// rule, on top of whatever an individual artifact asks for.
	ExcludeNamePattern []string
	MaxDepth           int

	// ShellUserHomes is the subset of UserHomes belonging to accounts with a
	// login shell, used by artifacts declaring exclude_nologin_users.
	ShellUserHomes []string

	// Account databases read from the *image*, not the host. This is what makes
	// no_user/no_group correct offline, where find(1) would consult the
	// examiner's own passwd file and report nonsense.
	UIDs map[uint32]bool
	GIDs map[uint32]bool

	// Mounts is the mount table, used to turn an artifact's
	// exclude_file_system into concrete paths to prune.
	Mounts mounts.Table

	// UserHomes expands %user_home%.
	UserHomes []string
	TempDir   string
	OutputDir string
}

Env carries everything a rule needs that is not on the file itself.

type Glob

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

Glob implements fnmatch(3) without FNM_PATHNAME, which is what find(1) uses for -path and -name: '*' spans '/' freely. Go's path/filepath.Match stops '*' at a separator, so it cannot be used here -- a pattern like "*/.git/hooks/*" would never match.

func CompileGlob

func CompileGlob(pat string) Glob

func (Glob) IsLiteral

func (g Glob) IsLiteral() bool

IsLiteral reports whether the pattern has no metacharacters.

func (Glob) Match

func (g Glob) Match(s string) bool

func (Glob) Pattern

func (g Glob) Pattern() string

type HistfileSpec

type HistfileSpec struct {
	// Var is the variable assigned in the rc file, e.g. HISTFILE.
	Var string
	// Anchored means the assignment has to start the line.
	Anchored bool
	// Files are the rc files to read, with %user_home% still unexpanded.
	Files []string
	// HomePlaceholder is what a leading "~/" is rewritten to, which UAC always
	// sets to the user home being iterated.
	HomePlaceholder string
}

HistfileSpec describes a recognised extraction command.

func ParseHistfileCommand

func ParseHistfileCommand(cmd string) (HistfileSpec, bool)

ParseHistfileCommand recognises the HISTFILE extraction shape. The second return value reports whether the command was understood; false means the artifact is one this tool does not implement, which the caller should treat as "skip", not as "collected nothing".

func (HistfileSpec) ExtractAssignments

func (s HistfileSpec) ExtractAssignments(content []byte) []string

ExtractAssignments pulls the assigned values out of an rc file's contents, applying the same transformation the shell pipeline would: everything up to and including the last "VAR=" on the line is dropped.

type Kind

type Kind string

Kind is the collector action a rule triggers.

const (
	KindFile Kind = "file" // copy the bytes out
	KindFind Kind = "find" // record the path
	KindStat Kind = "stat" // emit a bodyfile line
	KindHash Kind = "hash" // digest the contents

	// KindList is not a UAC collector name. It is what a recognised
	// HISTFILE-extraction command compiles to: a rule that reads matching rc
	// files during the walk and contributes the paths it finds to a list some
	// is_file_list artifact then collects.
	KindList Kind = "list"
)
const KindBodyfileLists Kind = "bodyfile_lists"

KindBodyfileLists is not a UAC collector name. It is the native reimplementation of bin/bodyfile2filelists.sh, UAC's own command collector that classifies every bodyfile entry into fourteen categories in a single pass -- sockets, hidden files/directories, suid/sgid, world/group-writable files/directories (plus a non-sticky-directory subset of world-writable), and files/directories owned by an unknown user or group.

Reimplementing it natively rather than treating it as an ordinary out-of-scope command collector matters for a reason discovered by running a real acquisition against a live root filesystem and diffing it against this exact artifact: bodyfile2filelists.yaml always runs before the standalone per-category YAML artifacts in every UAC profile that includes both, and each of those standalone artifacts is condition-gated to skip once bodyfile2filelists.sh has already written their output file --

condition: if [ ! -f ".../world_writable_files.txt" ]; then true; else false; fi

So in a real UAC run those standalone artifacts never actually execute. That matters because several of them declare the wrong permission bits when they DO run: system/world_writable_files.yaml checks permissions: [-0004] (world-READ) and system/group_writable_files.yaml checks [-0040] (group-READ), not the write bits their names promise. bodyfile2filelists.sh itself checks the write bits correctly. This is presumably a dormant, long-unnoticed inconsistency in upstream UAC's own artifact corpus -- it can only ever surface by literally running those artifacts, which normal UAC never does.

uacscan has no evaluator for arbitrary shell `condition:` clauses, and building one for this single, well-understood dependency would be a disproportionate amount of machinery for what is really one specific, verified fact about how the offline profile is put together. So that fact is encoded directly: recognise bodyfile2filelists.yaml, reimplement its classification logic against the same per-file data every other rule already sees during the walk, and drop the twelve artifacts it shadows.

type Rule

type Rule struct {
	ID     string
	Source string
	Kind   Kind

	OutputDir  string
	OutputFile string

	// Command is carried through for the two find+command artifacts, whose
	// per-file work (getcap, lsattr) the collectors implement natively.
	Command string

	// Histfile is set on KindList rules: what to extract and how.
	Histfile HistfileSpec

	// ListKey ties a producer to its consumer. On a KindList rule it is where
	// the list would have been written; on a file rule built from
	// is_file_list it is where the list is read from.
	ListKey string

	// FromList marks a rule whose paths come from a list produced during the
	// walk rather than from matching files as they are visited.
	FromList bool

	// Homes is the set of user home directories, needed to resolve a "~/"
	// value found in a system-wide rc file, where the owning user is not
	// implied by the path.
	Homes []string
	// contains filtered or unexported fields
}

Rule is one compiled artifact entry.

func ApplyBodyfileListsShadowing

func ApplyBodyfileListsShadowing(rs []*Rule) []*Rule

ApplyBodyfileListsShadowing mirrors what a real UAC run actually does when both bodyfile2filelists.yaml and bodyfile/bodyfile.yaml are selected: the twelve artifacts it shadows are dropped, matching the condition-gated skip they would hit for real. If bodyfile2filelists is selected but bodyfile/bodyfile.yaml is not, UAC's own condition on the artifact -- "if [ -s bodyfile.txt ]" -- would be false (there would be no bodyfile to classify), so the KindBodyfileLists rule is dropped instead and the standalone artifacts are left to run, matching that fallback path exactly.

This has to run after every rule in the set is compiled: which artifacts are shadowed depends on which OTHER artifacts were also selected, which is not knowable from any single artifact's own YAML.

func Compile

func Compile(e artifact.Entry, doc *artifact.Doc, env *Env) (*Rule, error)

Compile turns one artifact entry into a rule. It returns nil for entries that cannot run offline (the command collector) or that need a phase this walk does not provide (is_file_list, whose paths come from other files' contents).

func (*Rule) Excluded

func (r *Rule) Excluded(path, name string) bool

Excluded reports whether the path is pruned by this rule's own exclusions. Callers use it on directories to avoid descending.

func (*Rule) InScope

func (r *Rule) InScope(path string) (depth int, ok bool)

InScope reports whether the path lies at or beneath one of the rule's anchors, and the depth relative to that anchor. This is the walk-time equivalent of naming the path on find's command line.

func (*Rule) LiteralPrefixes

func (r *Rule) LiteralPrefixes() []string

LiteralPrefixes returns the non-glob leading path prefixes of this rule's anchors. The walker uses them to skip whole subtrees no rule can match.

func (*Rule) Match

func (r *Rule) Match(f *fsref.FileRef, env *Env) bool

Match answers the question find would have answered for this file.

type Set

type Set struct {
	Rules []*Rule
	// contains filtered or unexported fields
}

Set is a compiled collection of rules with a cheap first-pass filter.

func NewSet

func NewSet(rs []*Rule) *Set

func (*Set) MayContainMatches

func (s *Set) MayContainMatches(dir string) bool

MayContainMatches reports whether any rule could match something at or below dir. A false answer lets the walker skip the whole subtree.

Jump to

Keyboard shortcuts

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