Skip to content

Commit

Permalink
feat(suite/step/syscalls): add arguments constraints support
Browse files Browse the repository at this point in the history
Introduce mutually-exclusive-arguments, one-required-argument and
required-together-arguments constraints.
Mutually-exclusive-arguments constraint allows to mandate the user to
specify a value for at most one of a set of arguments.
One-required-argument constraint allows to mandate the user to specify
a value for at least one of a set of arguments.
Required-together-arguments constraints allows to mandate the user to
specify a value for each argument of a set of arguments if it provides
a value for at least one of them.

Signed-off-by: Leonardo Di Giovanna <leonardodigiovanna1@gmail.com>
  • Loading branch information
ekoops committed Mar 8, 2025
1 parent aaf411f commit 47c70ac
Show file tree
Hide file tree
Showing 19 changed files with 200 additions and 23 deletions.
187 changes: 182 additions & 5 deletions pkg/test/step/syscall/base/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,94 @@ import (
"context"
"fmt"
"reflect"
"slices"

"github.com/falcosecurity/event-generator/pkg/test/field"
"github.com/falcosecurity/event-generator/pkg/test/step"
)

// config is used to store syscall arguments configurations.
type config struct {
// defaultedArgs is the list of defaulted arguments.
defaultedArgs []string
// mutuallyExclusiveArgsLists is the list containing lists of mutually exclusive arguments.
mutuallyExclusiveArgsLists [][]string
// oneRequiredArgLists is the list containing lists of arguments such that at least one argument must be specified
// among the other arguments belonging to the same list.
oneRequiredArgLists [][]string
// requiredTogetherArgsLists is the list containing lists of arguments that must be specified all together if at
// least one of them is specified.
requiredTogetherArgsLists [][]string
}

// Option for configuring a Syscall.
type Option interface {
apply(*config) error
}

// funcOption is an implementation of Option storing a function that implements the requested apply method behavior.
type funcOption struct {
f func(*config) error
}

func (cfo *funcOption) apply(c *config) error {
return cfo.f(c)
}

// newFuncOption is a helper function to create a new funcOption from a function.
func newFuncOption(f func(*config) error) *funcOption {
return &funcOption{f: f}
}

var errArgsCannotBeEmpty = fmt.Errorf("arguments cannot be empty")

// WithDefaultedArgs allows to specify the list of defaulted arguments.
func WithDefaultedArgs(args []string) Option {
return newFuncOption(func(c *config) error {
if len(args) == 0 {
return errArgsCannotBeEmpty
}
c.defaultedArgs = args
return nil
})
}

// WithMutuallyExclusiveArgs puts a constraint on the provided arguments, mandating the user to specify a value for
// at most one of them. This option can be provided multiple times to enforce mutually-exclusive-arguments constraints
// upon different arguments list.
func WithMutuallyExclusiveArgs(args []string) Option {
return newFuncOption(func(c *config) error {
if len(args) == 0 {
return errArgsCannotBeEmpty
}
c.mutuallyExclusiveArgsLists = append(c.mutuallyExclusiveArgsLists, args)
return nil
})
}

// WithOneRequiredArg puts a constraint on the provided arguments, mandating the user to specify a value for at least
// one of them. This option can be provided multiple times to enforce one-required-argument constraints upon different
// arguments list.
func WithOneRequiredArg(args []string) Option {
return newFuncOption(func(c *config) error {
if len(args) == 0 {
return errArgsCannotBeEmpty
}
c.oneRequiredArgLists = append(c.oneRequiredArgLists, args)
return nil
})
}

// WithRequiredTogetherArgs puts a constraint on the provided arguments, mandating the user to specify a value for each
// of them if at least a value for one of them is specified. This option can be provided multiple times to enforce
// required-together-arguments constraints upon different arguments list.
func WithRequiredTogetherArgs(args []string) Option {
return newFuncOption(func(c *config) error {
c.requiredTogetherArgsLists = append(c.requiredTogetherArgsLists, args)
return nil
})
}

// Syscall provides a common implementation layer for system call test steps.
type Syscall struct {
// stepName is the syscall step name.
Expand All @@ -48,28 +131,39 @@ type Syscall struct {
fieldBindings []*step.FieldBinding
}

var errOpenModeMustBePositive = fmt.Errorf("open mode must be a positive integer")

// New creates a new system call test step common implementation layer.
func New(stepName string, rawArgs map[string]any, fieldBindings []*step.FieldBinding, argsContainer,
bindOnlyArgsContainer, retValueContainer reflect.Value, defaultedArgs []string) (*Syscall, error) {
bindOnlyArgsContainer, retValueContainer reflect.Value, options ...Option) (*Syscall, error) {
if err := checkContainersInvariants(argsContainer, bindOnlyArgsContainer, retValueContainer); err != nil {
return nil, err
}

unboundArgs := field.Paths(argsContainer.Type())
c := &config{}
for _, opt := range options {
if err := opt.apply(c); err != nil {
return nil, fmt.Errorf("error applying option: %w", err)
}
}

boundArgs, err := setArgFieldValues(argsContainer, rawArgs)
if err != nil {
return nil, fmt.Errorf("error setting argument field values: %w", err)
}

if err := checkArgsConstraints(c, boundArgs, fieldBindings); err != nil {
return nil, fmt.Errorf("error checking arguments constraints: %w", err)
}

// Evaluate unbound arguments sets.
unboundArgs := field.Paths(argsContainer.Type())
for _, boundArg := range boundArgs {
delete(unboundArgs, boundArg)
}

unboundBindOnlyArgs := field.Paths(bindOnlyArgsContainer.Type())

// Remove defaulted arguments from unbound arguments sets.
for _, arg := range defaultedArgs {
for _, arg := range c.defaultedArgs {
delete(unboundArgs, arg)
delete(unboundBindOnlyArgs, arg)
}
Expand All @@ -86,6 +180,7 @@ func New(stepName string, rawArgs map[string]any, fieldBindings []*step.FieldBin
return s, nil
}

// checkContainersInvariants verifies the requested assumptions on the provided containers.
func checkContainersInvariants(argsContainer, bindOnlyArgsContainer, retValueContainer reflect.Value) error {
if argsContainer.Kind() != reflect.Struct {
return fmt.Errorf("args container must be a struct")
Expand Down Expand Up @@ -125,6 +220,8 @@ func setArgFieldValues(argFieldContainer reflect.Value, rawArgs map[string]any)
return boundArgs, nil
}

var errOpenModeMustBePositive = fmt.Errorf("open mode must be a positive integer")

// setArgFieldValue sets the value of the provided field and/or sub-fields to the provided value, parsing it differently
// depending on the field type.
//
Expand Down Expand Up @@ -280,6 +377,86 @@ func setSubArgFieldValues(argField *field.Field, value any) ([]string, error) {
return boundArgs, nil
}

// checkArgsConstraints verifies that the provided bound arguments and field bindings meet the arguments constraints
// specified by the provided config.
func checkArgsConstraints(c *config, boundArgs []string, fieldBindings []*step.FieldBinding) error {
// Check mutually-exclusive-arguments constraints.
for _, args := range c.mutuallyExclusiveArgsLists {
if err := checkMutuallyExclusiveArgsConstraint(args, boundArgs, fieldBindings); err != nil {
return fmt.Errorf("error checking mutually-exclusive-arguments constraint for %v: %w", args, err)
}
}

// Check one-required-argument constraints.
for _, args := range c.oneRequiredArgLists {
if err := checkOneRequiredArgConstraint(args, boundArgs, fieldBindings); err != nil {
return fmt.Errorf("error checking one-required-argument constraint for %v: %w", args, err)
}
}

// Check required-together-arguments constraints.
for _, args := range c.requiredTogetherArgsLists {
if err := checkRequiredTogetherArgsConstraint(args, boundArgs, fieldBindings); err != nil {
return fmt.Errorf("error checking required-together-arguments constraint for %v: %w", args, err)
}
}

return nil
}

// checkMutuallyExclusiveArgsConstraint verifies that the provided bound arguments and the provided field bindings
// set/bind to at most one argument among the provided arguments. It returns an error if the condition is not met.
func checkMutuallyExclusiveArgsConstraint(args, boundArgs []string, fieldBindings []*step.FieldBinding) error {
matchingBoundArgs, matchingFieldBindings := getArgsMatches(args, boundArgs, fieldBindings)
if len(matchingBoundArgs)+len(matchingFieldBindings) > 1 {
return fmt.Errorf("found %v arguments set and %v field bindings", matchingBoundArgs, matchingFieldBindings)
}
return nil
}

// getArgsMatches returns the sub-list of provided bound arguments and the sub-list of provided field bindings matching
// the provided arguments.
func getArgsMatches(args, boundArgs []string, fieldBindings []*step.FieldBinding) (
matchingBoundArgs []string, matchingFieldBindings []string) {
for _, arg := range boundArgs {
if slices.Contains(args, arg) {
matchingBoundArgs = append(matchingBoundArgs, arg)
}
}

for _, fieldBinding := range fieldBindings {
arg := fieldBinding.LocalField
if slices.Contains(args, arg) {
matchingFieldBindings = append(matchingFieldBindings, arg)
}
}

return
}

var errOneRequiredArg = fmt.Errorf("at least one value must be specified for one of the arguments")

// checkOneRequiredArgConstraint verifies that the provided bound arguments and the provided field bindings set/bind to
// at least one argument among the provided arguments. It returns an error if the condition is not met.
func checkOneRequiredArgConstraint(args, boundArgs []string, fieldBindings []*step.FieldBinding) error {
matchingBoundArgs, matchingFieldBindings := getArgsMatches(args, boundArgs, fieldBindings)
if len(matchingBoundArgs)+len(matchingFieldBindings) == 0 {
return errOneRequiredArg
}
return nil
}

// checkRequiredTogetherArgsConstraint verifies that the provided bound arguments and the provided field bindings
// set/bind to all the provided arguments if at least one of the arguments is set/bind to a value. It returns an error
// if the condition is not met.
func checkRequiredTogetherArgsConstraint(args, boundArgs []string, fieldBindings []*step.FieldBinding) error {
matchingBoundArgs, matchingFieldBindings := getArgsMatches(args, boundArgs, fieldBindings)
if matchesNum := len(matchingBoundArgs) + len(matchingFieldBindings); matchesNum != 0 && matchesNum != len(args) {
return fmt.Errorf("found %v arguments set and %v field bindings", matchingBoundArgs, matchingFieldBindings)
}
return nil
}

// Name implements step.Step.Name method.
func (s *Syscall) Name() string {
return s.stepName
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/connect/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
bindOnlyArgsContainer := reflect.ValueOf(&c.bindOnlyArgs).Elem()
retValContainer := reflect.ValueOf(c).Elem()
var err error
c.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer, nil)
c.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/dup/dup.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
bindOnlyArgsContainer := reflect.ValueOf(&d.bindOnlyArgs).Elem()
retValContainer := reflect.ValueOf(d).Elem()
var err error
d.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer, nil)
d.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/dup2/dup2.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
bindOnlyArgsContainer := reflect.ValueOf(&d.bindOnlyArgs).Elem()
retValContainer := reflect.ValueOf(d).Elem()
var err error
d.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer, nil)
d.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/dup3/dup3.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
defaultedArgs := []string{"flags"}
var err error
d.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer,
defaultedArgs)
base.WithDefaultedArgs(defaultedArgs))
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/finitmodule/finitmodule.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
defaultedArgs := []string{"paramvalues", "flags"}
var err error
f.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer,
defaultedArgs)
base.WithDefaultedArgs(defaultedArgs))
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/initmodule/initmodule.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
defaultedArgs := []string{"paramvalues"}
var err error
i.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer,
defaultedArgs)
base.WithDefaultedArgs(defaultedArgs))
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/kill/kill.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
bindOnlyArgsContainer := reflect.ValueOf(&k.bindOnlyArgs).Elem()
retValContainer := reflect.ValueOf(k).Elem()
var err error
k.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer, nil)
k.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/link/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
bindOnlyArgsContainer := reflect.ValueOf(&l.bindOnlyArgs).Elem()
retValContainer := reflect.ValueOf(l).Elem()
var err error
l.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer, nil)
l.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/linkat/linkat.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
defaultedArgs := []string{"olddirfd", "newdirfd", "flags"}
var err error
l.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer,
defaultedArgs)
base.WithDefaultedArgs(defaultedArgs))
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/open/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
defaultedArgs := []string{"mode"}
var err error
o.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer,
defaultedArgs)
base.WithDefaultedArgs(defaultedArgs))
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/openat/openat.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
defaultedArgs := []string{"dirfd", "mode"}
var err error
o.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer,
defaultedArgs)
base.WithDefaultedArgs(defaultedArgs))
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/openat2/openat2.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
defaultedArgs := []string{"dirfd", "how", "how.flags", "how.mode", "how.resolve"}
var err error
o.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer,
defaultedArgs)
base.WithDefaultedArgs(defaultedArgs))
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/read/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
bindOnlyArgsContainer := reflect.ValueOf(&r.bindOnlyArgs).Elem()
retValContainer := reflect.ValueOf(r).Elem()
var err error
r.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer, nil)
r.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/sendto/sendto.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
defaultedArgs := []string{"len"}
var err error
s.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer,
defaultedArgs)
base.WithDefaultedArgs(defaultedArgs))
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/socket/socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
bindOnlyArgsContainer := reflect.ValueOf(&s.bindOnlyArgs).Elem()
retValContainer := reflect.ValueOf(s).Elem()
var err error
s.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer, nil)
s.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/symlink/symlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
bindOnlyArgsContainer := reflect.ValueOf(&s.bindOnlyArgs).Elem()
retValContainer := reflect.ValueOf(s).Elem()
var err error
s.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer, nil)
s.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/step/syscall/symlinkat/symlinkat.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func New(name string, rawArgs map[string]any, fieldBindings []*step.FieldBinding
defaultedArgs := []string{"newdirfd"}
var err error
s.Syscall, err = base.New(name, rawArgs, fieldBindings, argsContainer, bindOnlyArgsContainer, retValContainer,
defaultedArgs)
base.WithDefaultedArgs(defaultedArgs))
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 47c70ac

Please sign in to comment.