diff --git a/internal/factsengine/gatherers/gatherer.go b/internal/factsengine/gatherers/gatherer.go index cf71f9dc..5439d15f 100644 --- a/internal/factsengine/gatherers/gatherer.go +++ b/internal/factsengine/gatherers/gatherer.go @@ -24,6 +24,7 @@ func StandardGatherers() map[string]FactGatherer { SaptuneGathererName: NewDefaultSaptuneGatherer(), SBDConfigGathererName: NewDefaultSBDGatherer(), SBDDumpGathererName: NewDefaultSBDDumpGatherer(), + SysctlGathererName: NewDefaultSysctlGatherer(), SystemDGathererName: NewDefaultSystemDGatherer(), VerifyPasswordGathererName: NewDefaultPasswordGatherer(), } diff --git a/internal/factsengine/gatherers/sysctl.go b/internal/factsengine/gatherers/sysctl.go new file mode 100644 index 00000000..84808cfa --- /dev/null +++ b/internal/factsengine/gatherers/sysctl.go @@ -0,0 +1,108 @@ +package gatherers + +import ( + "strings" + + log "github.com/sirupsen/logrus" + "github.com/trento-project/agent/pkg/factsengine/entities" + "github.com/trento-project/agent/pkg/utils" +) + +const ( + SysctlGathererName = "sysctl" +) + +// nolint:gochecknoglobals +var ( + SysctlValueNotFound = entities.FactGatheringError{ + Type: "sysctl-value-not-found", + Message: "requested value not found in sysctl output", + } + + SysctlCommandError = entities.FactGatheringError{ + Type: "sysctl-cmd-error", + Message: "error executing sysctl command", + } + + SysctlMissingArgument = entities.FactGatheringError{ + Type: "sysctl-missing-argument", + Message: "missing required argument", + } +) + +type SysctlGatherer struct { + executor utils.CommandExecutor +} + +func NewDefaultSysctlGatherer() *SysctlGatherer { + return NewSysctlGatherer(utils.Executor{}) +} + +func NewSysctlGatherer(executor utils.CommandExecutor) *SysctlGatherer { + return &SysctlGatherer{ + executor: executor, + } +} + +func (s *SysctlGatherer) Gather(factsRequests []entities.FactRequest) ([]entities.Fact, error) { + facts := []entities.Fact{} + log.Infof("Starting %s facts gathering process", SysctlGathererName) + + output, err := s.executor.Exec("sysctl", "-a") + if err != nil { + return nil, SysctlCommandError.Wrap(err.Error()) + } + + sysctlMap := sysctlOutputToMap(output) + for _, factReq := range factsRequests { + var fact entities.Fact + + if len(factReq.Argument) == 0 { + log.Error(SysctlMissingArgument.Message) + fact = entities.NewFactGatheredWithError(factReq, &SysctlMissingArgument) + } else if value, err := sysctlMap.GetValue(factReq.Argument); err == nil { + fact = entities.NewFactGatheredWithRequest(factReq, value) + } else { + gatheringError := SysctlValueNotFound.Wrap(factReq.Argument) + log.Error(gatheringError) + fact = entities.NewFactGatheredWithError(factReq, gatheringError) + } + + facts = append(facts, fact) + } + + log.Infof("Requested %s facts gathered", SysctlGathererName) + return facts, nil +} + +func sysctlOutputToMap(output []byte) *entities.FactValueMap { + outputMap := &entities.FactValueMap{Value: make(map[string]entities.FactValue)} + + for _, line := range strings.Split(string(output), "\n") { + parts := strings.SplitN(line, "=", 2) + if len(line) == 0 || len(parts) != 2 { + log.Error("Invalid sysctl output line: ", line) + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + cursor := outputMap + pathComponents := strings.Split(key, ".") + + for i, component := range pathComponents { + if i == len(pathComponents)-1 { + cursor.Value[component] = entities.ParseStringToFactValue(value) + } else if nestedMap, ok := cursor.Value[component].(*entities.FactValueMap); !ok { + newMap := &entities.FactValueMap{Value: make(map[string]entities.FactValue)} + cursor.Value[component] = newMap + cursor = newMap + } else { + cursor = nestedMap + } + } + } + + return outputMap +} diff --git a/internal/factsengine/gatherers/sysctl_test.go b/internal/factsengine/gatherers/sysctl_test.go new file mode 100644 index 00000000..9a0d5fd2 --- /dev/null +++ b/internal/factsengine/gatherers/sysctl_test.go @@ -0,0 +1,217 @@ +package gatherers_test + +import ( + "io" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/trento-project/agent/internal/factsengine/gatherers" + "github.com/trento-project/agent/pkg/factsengine/entities" + utilsMocks "github.com/trento-project/agent/pkg/utils/mocks" + "github.com/trento-project/agent/test/helpers" +) + +type SysctlTestSuite struct { + suite.Suite + mockExecutor *utilsMocks.CommandExecutor +} + +func TestSysctlTestSuite(t *testing.T) { + suite.Run(t, new(SysctlTestSuite)) +} + +func (suite *SysctlTestSuite) SetupTest() { + suite.mockExecutor = new(utilsMocks.CommandExecutor) +} + +func (suite *SysctlTestSuite) TestSysctlGathererNoArgumentProvided() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) + + c := gatherers.NewSysctlGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "no_argument_fact", + Gatherer: "sysctl", + }, + { + Name: "empty_argument_fact", + Gatherer: "sysctl", + Argument: "", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "no_argument_fact", + Value: nil, + Error: &entities.FactGatheringError{ + Message: "missing required argument", + Type: "sysctl-missing-argument", + }, + }, + { + Name: "empty_argument_fact", + Value: nil, + Error: &entities.FactGatheringError{ + Message: "missing required argument", + Type: "sysctl-missing-argument", + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SysctlTestSuite) TestSysctlGathererNonExistingKey() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) + + c := gatherers.NewSysctlGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "madeup_fact", + Gatherer: "sysctl", + Argument: "madeup.fact", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "madeup_fact", + Value: nil, + Error: &entities.FactGatheringError{ + Message: "requested value not found in sysctl output: madeup.fact", + Type: "sysctl-value-not-found", + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SysctlTestSuite) TestSysctlCommandNotFound() { + suite.mockExecutor.On("Exec", "sysctl", "-a").Return(nil, exec.ErrNotFound) + + c := gatherers.NewSysctlGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "fs.inotify.max_user_watches", + Gatherer: "sysctl", + Argument: "fs.inotify.max_user_watches", + }, + } + + expectedError := &entities.FactGatheringError{ + Message: "error executing sysctl command: executable file not found in $PATH", + Type: "sysctl-cmd-error", + } + + factResults, err := c.Gather(factRequests) + + suite.EqualError(err, expectedError.Error()) + + suite.Empty(factResults) +} + +func (suite *SysctlTestSuite) TestSysctlGatherer() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) + + c := gatherers.NewSysctlGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "simple_value", + Gatherer: "sysctl", + Argument: "fs.inotify.max_user_watches", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "simple_value", + Value: &entities.FactValueInt{Value: 65536}, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SysctlTestSuite) TestSysctlGathererPartialKey() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) + + c := gatherers.NewSysctlGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "partial_key", + Gatherer: "sysctl", + Argument: "debug", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "partial_key", + Value: &entities.FactValueMap{ + Value: map[string]entities.FactValue{ + "exception-trace": &entities.FactValueInt{Value: 1}, + "kprobes-optimization": &entities.FactValueInt{Value: 1}, + }, + }, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} + +func (suite *SysctlTestSuite) TestSysctlGathererEmptyValue() { + mockOutputFile, _ := os.Open(helpers.GetFixturePath("gatherers/sysctl.output")) + mockOutput, _ := io.ReadAll(mockOutputFile) + suite.mockExecutor.On("Exec", "sysctl", "-a").Return(mockOutput, nil) + + c := gatherers.NewSysctlGatherer(suite.mockExecutor) + + factRequests := []entities.FactRequest{ + { + Name: "empty_value", + Gatherer: "sysctl", + Argument: "kernel.domainname", + }, + } + + factResults, err := c.Gather(factRequests) + + expectedResults := []entities.Fact{ + { + Name: "empty_value", + Value: &entities.FactValueString{Value: ""}, + }, + } + + suite.NoError(err) + suite.ElementsMatch(expectedResults, factResults) +} diff --git a/test/fixtures/gatherers/sysctl.output b/test/fixtures/gatherers/sysctl.output new file mode 100644 index 00000000..f0b39075 --- /dev/null +++ b/test/fixtures/gatherers/sysctl.output @@ -0,0 +1,148 @@ +abi.vsyscall32 = 1 +debug.exception-trace = 1 +debug.kprobes-optimization = 1 +dev.hpet.max-user-freq = 64 +dev.scsi.logging_level = 0 +dev.tty.ldisc_autoload = 1 +fs.aio-max-nr = 65536 +fs.aio-nr = 0 +fs.binfmt_misc.status = enabled +fs.dentry-state = 232452 206256 45 0 172158 0 +fs.dir-notify-enable = 1 +fs.epoll.max_user_watches = 411258 +fs.file-max = 200400 +fs.file-nr = 3040 0 200400 +fs.inode-nr = 65123 4352 +fs.inode-state = 65123 4352 0 0 0 0 0 +fs.inotify.max_queued_events = 16384 +fs.inotify.max_user_instances = 128 +fs.inotify.max_user_watches = 65536 +fs.lease-break-time = 45 +fs.leases-enable = 1 +fs.mount-max = 100000 +fs.mqueue.msg_default = 10 +fs.mqueue.msg_max = 10 +fs.mqueue.msgsize_default = 8192 +fs.mqueue.msgsize_max = 8192 +fs.mqueue.queues_max = 256 +fs.nr_open = 1048576 +fs.overflowgid = 65534 +fs.overflowuid = 65534 +fs.pipe-max-size = 1048576 +fs.pipe-user-pages-hard = 0 +fs.pipe-user-pages-soft = 16384 +fs.procfs-drop-fd-dentries = 0 +fs.protected_fifos = 0 +fs.protected_hardlinks = 1 +fs.protected_regular = 0 +fs.protected_symlinks = 1 +fs.quota.allocated_dquots = 0 +fs.quota.cache_hits = 0 +fs.quota.drops = 0 +fs.quota.free_dquots = 0 +fs.quota.lookups = 0 +fs.quota.reads = 0 +fs.quota.syncs = 76 +fs.quota.warnings = 1 +fs.quota.writes = 0 +fs.suid_dumpable = 0 +kernel.acct = 4 2 30 +kernel.acpi_video_flags = 0 +kernel.auto_msgmni = 0 +kernel.bootloader_type = 114 +kernel.domainname = +kernel.sched_domain.cpu1.domain0.max_interval = 4 +kernel.sched_domain.cpu1.domain0.max_newidle_lb_cost = 142930 +kernel.sched_domain.cpu1.domain0.min_interval = 2 +kernel.sched_domain.cpu1.domain0.name = DIE +kernel.sched_energy_aware = 1 +kernel.sched_latency_ns = 12000000 +kernel.sched_migration_cost_ns = 500000 +kernel.sched_min_granularity_ns = 4000000 +kernel.sched_nr_migrate = 32 +kernel.sched_rr_timeslice_ms = 100 +kernel.sched_rt_period_us = 1000000 +kernel.sched_rt_runtime_us = 950000 +kernel.sched_schedstats = 0 +kernel.sched_tunable_scaling = 1 +kernel.sched_wakeup_granularity_ns = 5000000 +kernel.seccomp.actions_avail = kill_process kill_thread trap errno user_notif trace log allow +kernel.seccomp.actions_logged = kill_process kill_thread trap errno user_notif trace log +kernel.sem = 32000 1024000000 500 32000 +kernel.sem_next_id = -1 +kernel.shm_next_id = -1 +kernel.shm_rmid_forced = 0 +kernel.shmall = 1152921504606846720 +kernel.shmmax = 18446744073709551615 +kernel.shmmni = 4096 +kernel.soft_watchdog = 1 +kernel.softlockup_all_cpu_backtrace = 0 +kernel.softlockup_panic = 0 +kernel.stack_tracer_enabled = 0 +kernel.suid_dumpable = 0 +kernel.sysctl_writes_strict = 1 +kernel.sysrq = 184 +kernel.tainted = 0 +kernel.threads-max = 15688 +kernel.timer_migration = 1 +kernel.traceoff_on_warning = 0 +kernel.tracepoint_printk = 0 +kernel.unknown_nmi_panic = 0 +kernel.unprivileged_bpf_disabled = 2 +kernel.unprivileged_userns_apparmor_policy = 1 +kernel.usermodehelper.bset = 4294967295 63 +kernel.usermodehelper.inheritable = 4294967295 63 +kernel.version = #1 SMP Mon Nov 22 08:38:17 UTC 2021 (52078fe) +kernel.watchdog = 1 +kernel.watchdog_cpumask = 0-1 +kernel.watchdog_thresh = 10 +net.bridge.bridge-nf-call-arptables = 1 +net.bridge.bridge-nf-call-ip6tables = 1 +net.bridge.bridge-nf-call-iptables = 1 +net.bridge.bridge-nf-filter-pppoe-tagged = 0 +net.bridge.bridge-nf-filter-vlan-tagged = 0 +net.bridge.bridge-nf-pass-vlan-input-dev = 0 +net.core.bpf_jit_enable = 1 +net.core.bpf_jit_harden = 0 +net.core.bpf_jit_kallsyms = 0 +net.core.bpf_jit_limit = 264241152 +net.core.busy_poll = 0 +net.core.busy_read = 0 +net.core.default_qdisc = pfifo_fast +net.core.dev_weight = 64 +net.core.dev_weight_rx_bias = 1 +net.core.somaxconn = 128 +net.core.tstamp_allow_data = 1 +net.core.warnings = 0 +net.core.wmem_default = 212992 +net.core.wmem_max = 212992 +net.core.xfrm_acq_expires = 30 +net.core.xfrm_aevent_etime = 10 +net.core.xfrm_aevent_rseqth = 2 +net.core.xfrm_larval_drop = 1 +net.ipv4.cipso_cache_bucket_size = 10 +net.ipv4.cipso_cache_enable = 1 +net.ipv4.cipso_rbm_optfmt = 0 +net.ipv4.cipso_rbm_strictvalid = 1 +net.ipv4.conf.all.accept_local = 0 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv6.conf.docker0.keep_addr_on_down = 0 +net.ipv6.conf.docker0.max_addresses = 16 +net.netfilter.nf_conntrack_expect_max = 256 +net.netfilter.nf_conntrack_frag6_high_thresh = 4194304 +net.netfilter.nf_conntrack_frag6_low_thresh = 3145728 +net.netfilter.nf_conntrack_frag6_timeout = 60 +net.netfilter.nf_conntrack_generic_timeout = 600 +user.max_net_namespaces = 7844 +user.max_pid_namespaces = 7844 +user.max_time_namespaces = 7844 +user.max_user_namespaces = 7844 +user.max_uts_namespaces = 7844 +vm.admin_reserve_kbytes = 8192 +vm.block_dump = 0 +vm.compact_unevictable_allowed = 1 +vm.dirty_background_bytes = 0 +vm.dirty_background_ratio = 10 +vm.dirty_bytes = 0 +vm.dirty_expire_centisecs = 3000 +vm.dirty_ratio = 20