diff --git a/cmd/main.go b/cmd/main.go index 0545fda174..eb828460c6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -33,6 +33,7 @@ import ( "github.com/kubescape/node-agent/pkg/exporters" "github.com/kubescape/node-agent/pkg/fimmanager" "github.com/kubescape/node-agent/pkg/healthmanager" + hostsensormanager "github.com/kubescape/node-agent/pkg/hostsensormanager" "github.com/kubescape/node-agent/pkg/malwaremanager" malwaremanagerv1 "github.com/kubescape/node-agent/pkg/malwaremanager/v1" "github.com/kubescape/node-agent/pkg/metricsmanager" @@ -181,6 +182,22 @@ func main() { } dWatcher.AddAdaptor(k8sObjectCache) + // Create the host sensor manager + var hostSensorManager hostsensormanager.HostSensorManager + if cfg.EnableHostSensor { + hostSensorConfig := hostsensormanager.Config{ + Enabled: cfg.EnableHostSensor, + Interval: cfg.HostSensorInterval, + NodeName: cfg.NodeName, + } + hostSensorManager, err = hostsensormanager.NewHostSensorManager(hostSensorConfig) + if err != nil { + logger.L().Ctx(ctx).Fatal("error creating HostSensorManager", helpers.Error(err)) + } + } else { + hostSensorManager = hostsensormanager.NewNoopHostSensorManager() + } + // Create the seccomp manager var seccompManager seccompmanager.SeccompManagerClient var seccompWatcher seccompprofilewatcher.SeccompProfileWatcher @@ -397,6 +414,12 @@ func main() { // Start the prometheusExporter prometheusExporter.Start() + // Start the host sensor manager + if err = hostSensorManager.Start(ctx); err != nil { + logger.L().Ctx(ctx).Fatal("error starting host sensor manager", helpers.Error(err)) + } + defer hostSensorManager.Stop() + // Start the FIM manager if fimManager != nil { err = fimManager.Start(ctx) diff --git a/configuration/config.json b/configuration/config.json index 1319d54fb4..055df2717e 100644 --- a/configuration/config.json +++ b/configuration/config.json @@ -95,5 +95,7 @@ "exporters": { "stdoutExporter": true } - } + }, + "hostSensorEnabled": true, + "hostSensorInterval": "1m" } \ No newline at end of file diff --git a/go.mod b/go.mod index 684ea733fc..bd5849ca88 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,7 @@ require ( github.com/spf13/afero v1.15.0 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + github.com/weaveworks/procspy v0.0.0-20150706124340-cb970aa190c3 go.uber.org/multierr v1.11.0 golang.org/x/net v0.47.0 golang.org/x/sys v0.38.0 diff --git a/go.sum b/go.sum index 4249ce6e70..5d7eec1ced 100644 --- a/go.sum +++ b/go.sum @@ -1936,6 +1936,8 @@ github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIq github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8= github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= +github.com/weaveworks/procspy v0.0.0-20150706124340-cb970aa190c3 h1:UC4iN/yCDCObTBhKzo34/R2U6qptTPmqbzG6UiQVMUQ= +github.com/weaveworks/procspy v0.0.0-20150706124340-cb970aa190c3/go.mod h1:cJTfuBcxkdbj8Mabk4PPdaf0AXv9TYEJmkFxKcWxYY4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= diff --git a/pkg/config/config.go b/pkg/config/config.go index 948ae5f288..797288c06a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -98,6 +98,9 @@ type Config struct { UpdateDataPeriod time.Duration `mapstructure:"updateDataPeriod"` WorkerChannelSize int `mapstructure:"workerChannelSize"` WorkerPoolSize int `mapstructure:"workerPoolSize"` + // Host sensor configuration + EnableHostSensor bool `mapstructure:"hostSensorEnabled"` + HostSensorInterval time.Duration `mapstructure:"hostSensorInterval"` } // FIMConfig defines the configuration for File Integrity Monitoring @@ -200,6 +203,9 @@ func LoadConfig(path string) (Config, error) { viper.SetDefault("fim::periodicConfig::maxFileSize", int64(100*1024*1024)) viper.SetDefault("fim::periodicConfig::followSymlinks", false) viper.SetDefault("fim::exporters::stdoutExporter", false) + // Host sensor defaults + viper.SetDefault("hostSensorEnabled", true) + viper.SetDefault("hostSensorInterval", 5*time.Minute) viper.AutomaticEnv() diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index aa3c4cdffe..c2afc72450 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -39,6 +39,8 @@ func TestLoadConfig(t *testing.T) { EnableFIM: true, EnableNetworkStreaming: false, EnableEmbeddedSboms: false, + EnableHostSensor: true, + HostSensorInterval: 1 * time.Minute, KubernetesMode: true, NetworkStreamingInterval: 2 * time.Minute, InitialDelay: 2 * time.Minute, diff --git a/pkg/hostsensormanager/crd_client.go b/pkg/hostsensormanager/crd_client.go new file mode 100644 index 0000000000..e65e223f12 --- /dev/null +++ b/pkg/hostsensormanager/crd_client.go @@ -0,0 +1,147 @@ +package hostsensormanager + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" +) + +// CRDClient handles Kubernetes CRD operations +type CRDClient struct { + dynamicClient dynamic.Interface + nodeName string +} + +// NewCRDClient creates a new CRD client +func NewCRDClient(nodeName string) (*CRDClient, error) { + config, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("failed to get in-cluster config: %w", err) + } + + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create dynamic client: %w", err) + } + + return &CRDClient{ + dynamicClient: dynamicClient, + nodeName: nodeName, + }, nil +} + +// CreateOrUpdateHostData creates or updates a host data CRD +func (c *CRDClient) CreateOrUpdateHostData(ctx context.Context, resource string, kind string, spec interface{}) error { + gvr := schema.GroupVersionResource{ + Group: HostDataGroup, + Version: HostDataVersion, + Resource: resource, + } + + // Create the unstructured object + unstructuredObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": fmt.Sprintf("%s/%s", HostDataGroup, HostDataVersion), + "kind": kind, + "metadata": map[string]interface{}{ + "name": c.nodeName, + }, + "spec": spec, + "status": map[string]interface{}{ + "lastSensed": metav1.Now().UTC().Format(time.RFC3339), + }, + }, + } + + // Try to create the resource + _, err := c.dynamicClient.Resource(gvr).Create(ctx, unstructuredObj, metav1.CreateOptions{}) + if err == nil { + logger.L().Info("created host data CRD", + helpers.String("kind", kind), + helpers.String("nodeName", c.nodeName)) + return nil + } + + // If it already exists, update it + if errors.IsAlreadyExists(err) { + logger.L().Debug("host data CRD already exists, updating", + helpers.String("kind", kind), + helpers.String("nodeName", c.nodeName)) + + // Get the existing resource to get the resource version + existing, err := c.dynamicClient.Resource(gvr).Get(ctx, c.nodeName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get existing %s CRD: %w", kind, err) + } + + // Update the object with the resource version and other metadata + unstructuredObj.SetResourceVersion(existing.GetResourceVersion()) + unstructuredObj.SetUID(existing.GetUID()) + + // Update the resource + _, err = c.dynamicClient.Resource(gvr).Update(ctx, unstructuredObj, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update %s CRD: %w", kind, err) + } + + logger.L().Debug("updated host data CRD", + helpers.String("kind", kind), + helpers.String("nodeName", c.nodeName)) + return nil + } + + return fmt.Errorf("failed to create %s CRD: %w", kind, err) +} + +// UpdateStatus updates the status of a host data CRD with an error +func (c *CRDClient) UpdateStatus(ctx context.Context, resource string, errorMsg string) error { + gvr := schema.GroupVersionResource{ + Group: HostDataGroup, + Version: HostDataVersion, + Resource: resource, + } + + patchData, err := json.Marshal(map[string]interface{}{ + "status": Status{ + LastSensed: metav1.Now(), + Error: errorMsg, + }, + }) + if err != nil { + return fmt.Errorf("failed to marshal patch data: %w", err) + } + + _, err = c.dynamicClient.Resource(gvr).Patch(ctx, c.nodeName, types.MergePatchType, patchData, metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("failed to update status: %w", err) + } + + return nil +} + +// toUnstructured converts a typed object to unstructured +func toUnstructured(obj interface{}) (*unstructured.Unstructured, error) { + data, err := json.Marshal(obj) + if err != nil { + return nil, err + } + + var unstructuredObj unstructured.Unstructured + err = json.Unmarshal(data, &unstructuredObj.Object) + if err != nil { + return nil, err + } + + return &unstructuredObj, nil +} diff --git a/pkg/hostsensormanager/manager.go b/pkg/hostsensormanager/manager.go new file mode 100644 index 0000000000..4c9d3f5875 --- /dev/null +++ b/pkg/hostsensormanager/manager.go @@ -0,0 +1,173 @@ +package hostsensormanager + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +// manager implements the HostSensorManager interface +type manager struct { + config Config + crdClient *CRDClient + sensors []Sensor + stopCh chan struct{} + wg sync.WaitGroup + startOnce sync.Once +} + +// NewHostSensorManager creates a new host sensor manager +func NewHostSensorManager(config Config) (HostSensorManager, error) { + if !config.Enabled { + return NewNoopHostSensorManager(), nil + } + + if config.NodeName == "" { + return nil, fmt.Errorf("node name is required") + } + + if config.Interval == 0 { + config.Interval = 5 * time.Minute // Default to 5 minutes + } + + crdClient, err := NewCRDClient(config.NodeName) + if err != nil { + return nil, fmt.Errorf("failed to create CRD client: %w", err) + } + + // Initialize sensors + sensors := []Sensor{ + NewOsReleaseSensor(config.NodeName), + NewKernelVersionSensor(config.NodeName), + NewLinuxSecurityHardeningSensor(config.NodeName), + NewOpenPortsSensor(config.NodeName), + NewLinuxKernelVariablesSensor(config.NodeName), + NewKubeletInfoSensor(config.NodeName), + NewKubeProxyInfoSensor(config.NodeName), + NewControlPlaneInfoSensor(config.NodeName), + NewCloudProviderInfoSensor(config.NodeName), + NewCNIInfoSensor(config.NodeName), + } + + return &manager{ + config: config, + crdClient: crdClient, + sensors: sensors, + stopCh: make(chan struct{}), + }, nil +} + +// Start begins the sensing loop +func (m *manager) Start(ctx context.Context) error { + m.startOnce.Do(func() { + logger.L().Info("starting host sensor manager", + helpers.String("nodeName", m.config.NodeName), + helpers.String("interval", m.config.Interval.String())) + + // Run initial sensing immediately + m.runSensing(ctx) + + // Start periodic sensing + m.wg.Add(1) + go m.sensingLoop(ctx) + }) + + return nil +} + +// Stop gracefully stops the manager +func (m *manager) Stop() error { + logger.L().Info("stopping host sensor manager") + select { + case <-m.stopCh: + // Already closed + return nil + default: + close(m.stopCh) + } + m.wg.Wait() + logger.L().Info("host sensor manager stopped") + return nil +} + +// sensingLoop runs the periodic sensing +func (m *manager) sensingLoop(ctx context.Context) { + defer m.wg.Done() + + ticker := time.NewTicker(m.config.Interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + logger.L().Info("context cancelled, stopping sensing loop") + return + case <-m.stopCh: + logger.L().Info("stop signal received, stopping sensing loop") + return + case <-ticker.C: + m.runSensing(ctx) + } + } +} + +// runSensing executes all sensors and updates CRDs +func (m *manager) runSensing(ctx context.Context) { + logger.L().Debug("running host sensors", helpers.Int("sensorCount", len(m.sensors))) + + for _, sensor := range m.sensors { + if err := m.runSensor(ctx, sensor); err != nil { + logger.L().Warning("sensor failed", + helpers.String("kind", sensor.GetKind()), + helpers.Error(err)) + } + } +} + +// runSensor executes a single sensor and updates its CRD +func (m *manager) runSensor(ctx context.Context, sensor Sensor) error { + logger.L().Debug("running sensor", helpers.String("kind", sensor.GetKind())) + + // Map Kind to Resource name (plural, lowercase) + resource := sensor.GetPluralKind() + + // Sense the data + data, err := sensor.Sense() + if err != nil { + // Update status with error + if updateErr := m.crdClient.UpdateStatus(ctx, resource, err.Error()); updateErr != nil { + logger.L().Warning("failed to update CRD status", + helpers.String("kind", sensor.GetKind()), + helpers.Error(updateErr)) + } + return fmt.Errorf("failed to sense data: %w", err) + } + + // Update CRD + if err := m.crdClient.CreateOrUpdateHostData(ctx, resource, sensor.GetKind(), data); err != nil { + return fmt.Errorf("failed to create/update CRD: %w", err) + } + + logger.L().Debug("sensor completed successfully", helpers.String("kind", sensor.GetKind())) + return nil +} + +// noopManager is a no-op implementation when the manager is disabled +type noopManager struct{} + +// NewNoopHostSensorManager creates a new no-op host sensor manager +func NewNoopHostSensorManager() HostSensorManager { + return &noopManager{} +} + +func (n *noopManager) Start(ctx context.Context) error { + return nil +} + +func (n *noopManager) Stop() error { + return nil +} diff --git a/pkg/hostsensormanager/sensor_cloudprovider.go b/pkg/hostsensormanager/sensor_cloudprovider.go new file mode 100644 index 0000000000..f6aad45727 --- /dev/null +++ b/pkg/hostsensormanager/sensor_cloudprovider.go @@ -0,0 +1,84 @@ +package hostsensormanager + +import ( + "net/http" + "time" +) + +// CloudProviderInfoSensor implements the Sensor interface for cloud provider info data +type CloudProviderInfoSensor struct { + nodeName string +} + +// NewCloudProviderInfoSensor creates a new cloud provider info sensor +func NewCloudProviderInfoSensor(nodeName string) *CloudProviderInfoSensor { + return &CloudProviderInfoSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *CloudProviderInfoSensor) GetKind() string { + return "CloudProviderInfo" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *CloudProviderInfoSensor) GetPluralKind() string { + return "cloudproviderinfos" +} + +// Sense collects the cloud provider info data from the host +func (s *CloudProviderInfoSensor) Sense() (interface{}, error) { + ret := CloudProviderInfoSpec{ + ProviderMetaDataAPIAccess: s.hasMetaDataAPIAccess(), + NodeName: s.nodeName, + } + + return &ret, nil +} + +type apisURL struct { + url string + headers map[string]string +} + +var cloudProviderMetaDataAPIs = []apisURL{ + { + "http://169.254.169.254/computeMetadata/v1/?alt=json&recursive=true", + map[string]string{"Metadata-Flavor": "Google"}, + }, + { + "http://169.254.169.254/metadata/instance?api-version=2021-02-01", + map[string]string{"Metadata": "true"}, + }, + { + "http://169.254.169.254/latest/meta-data/local-hostname", + map[string]string{}, + }, +} + +func (s *CloudProviderInfoSensor) hasMetaDataAPIAccess() bool { + client := &http.Client{ + Timeout: time.Second, + } + + for _, req := range cloudProviderMetaDataAPIs { + httpReq, err := http.NewRequest("GET", req.url, nil) + if err != nil { + continue + } + for k, v := range req.headers { + httpReq.Header.Set(k, v) + } + + res, err := client.Do(httpReq) + if err == nil { + defer res.Body.Close() + if res.StatusCode == http.StatusOK { + return true + } + } + } + + return false +} diff --git a/pkg/hostsensormanager/sensor_cni.go b/pkg/hostsensormanager/sensor_cni.go new file mode 100644 index 0000000000..bf86a9d0e2 --- /dev/null +++ b/pkg/hostsensormanager/sensor_cni.go @@ -0,0 +1,81 @@ +package hostsensormanager + +import ( + "context" + + "github.com/kubescape/go-logger" +) + +// CNIInfoSensor implements the Sensor interface for CNI info data +type CNIInfoSensor struct { + nodeName string +} + +// NewCNIInfoSensor creates a new CNI info sensor +func NewCNIInfoSensor(nodeName string) *CNIInfoSensor { + return &CNIInfoSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *CNIInfoSensor) GetKind() string { + return "CNIInfo" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *CNIInfoSensor) GetPluralKind() string { + return "cniinfos" +} + +// Sense collects the CNI info data from the host +func (s *CNIInfoSensor) Sense() (interface{}, error) { + ctx := context.Background() + ret := CNIInfoSpec{ + NodeName: s.nodeName, + } + + // Simplified CNI config path collection for now + // host-scanner uses Kubelet process to find CNI path, but we can try some defaults + cniConfigDirs := []string{"/etc/cni/net.d"} + + for _, dir := range cniConfigDirs { + infos, err := makeHostDirFilesInfoVerbose(ctx, dir, true, 0) + if err == nil { + ret.CNIConfigFiles = append(ret.CNIConfigFiles, infos...) + } + } + + ret.CNINames = s.getCNINames() + + return &ret, nil +} + +func (s *CNIInfoSensor) getCNINames() []string { + var CNIs []string + supportedCNIs := []struct { + name string + processSuffix string + }{ + {"aws", "aws-k8s-agent"}, + {"Calico", "calico-node"}, + {"Flannel", "flanneld"}, + {"Cilium", "cilium-agent"}, + {"WeaveNet", "weave-net"}, + {"Kindnet", "kindnetd"}, + {"Multus", "multus"}, + } + + for _, cni := range supportedCNIs { + p, _ := LocateProcessByExecSuffix(cni.processSuffix) + if p != nil { + CNIs = append(CNIs, cni.name) + } + } + + if len(CNIs) == 0 { + logger.L().Debug("no CNI found") + } + + return CNIs +} diff --git a/pkg/hostsensormanager/sensor_controlplane.go b/pkg/hostsensormanager/sensor_controlplane.go new file mode 100644 index 0000000000..880fa2150c --- /dev/null +++ b/pkg/hostsensormanager/sensor_controlplane.go @@ -0,0 +1,89 @@ +package hostsensormanager + +import ( + "context" +) + +const ( + apiServerExe = "/kube-apiserver" + controllerManagerExe = "/kube-controller-manager" + schedulerExe = "/kube-scheduler" + etcdExe = "/etcd" + etcdDataDirArg = "--data-dir" + auditPolicyFileArg = "--audit-policy-file" + + apiServerSpecsPath = "/etc/kubernetes/manifests/kube-apiserver.yaml" + controllerManagerSpecsPath = "/etc/kubernetes/manifests/kube-controller-manager.yaml" + schedulerSpecsPath = "/etc/kubernetes/manifests/kube-scheduler.yaml" + etcdConfigPath = "/etc/kubernetes/manifests/etcd.yaml" + adminConfigPath = "/etc/kubernetes/admin.conf" + pkiDir = "/etc/kubernetes/pki" +) + +// ControlPlaneInfoSensor implements the Sensor interface for control plane info data +type ControlPlaneInfoSensor struct { + nodeName string +} + +// NewControlPlaneInfoSensor creates a new control plane info sensor +func NewControlPlaneInfoSensor(nodeName string) *ControlPlaneInfoSensor { + return &ControlPlaneInfoSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *ControlPlaneInfoSensor) GetKind() string { + return "ControlPlaneInfo" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *ControlPlaneInfoSensor) GetPluralKind() string { + return "controlplaneinfos" +} + +// Sense collects the control plane info data from the host +func (s *ControlPlaneInfoSensor) Sense() (interface{}, error) { + ctx := context.Background() + ret := ControlPlaneInfoSpec{ + NodeName: s.nodeName, + } + + // API Server + if proc, err := LocateProcessByExecSuffix(apiServerExe); err == nil { + ret.APIServerInfo = &ApiServerInfo{ + ProcessInfo: ProcessInfo{ + CmdLine: proc.RawCmd(), + SpecsFile: makeHostFileInfoVerbose(ctx, apiServerSpecsPath, false), + }, + AuditPolicyFile: makeContaineredFileInfoVerbose(ctx, proc, auditPolicyFileArg, false), + } + } + + // Controller Manager + if proc, err := LocateProcessByExecSuffix(controllerManagerExe); err == nil { + ret.ControllerManagerInfo = &ProcessInfo{ + CmdLine: proc.RawCmd(), + SpecsFile: makeHostFileInfoVerbose(ctx, controllerManagerSpecsPath, false), + } + } + + // Scheduler + if proc, err := LocateProcessByExecSuffix(schedulerExe); err == nil { + ret.SchedulerInfo = &ProcessInfo{ + CmdLine: proc.RawCmd(), + SpecsFile: makeHostFileInfoVerbose(ctx, schedulerSpecsPath, false), + } + } + + // Other configs + ret.EtcdConfigFile = makeHostFileInfoVerbose(ctx, etcdConfigPath, false) + ret.AdminConfigFile = makeHostFileInfoVerbose(ctx, adminConfigPath, false) + ret.PKIDir = makeHostFileInfoVerbose(ctx, pkiDir, false) + + // PKI files + pkiFiles, _ := makeHostDirFilesInfoVerbose(ctx, pkiDir, true, 0) + ret.PKIFiles = pkiFiles + + return &ret, nil +} diff --git a/pkg/hostsensormanager/sensor_kernelvars.go b/pkg/hostsensormanager/sensor_kernelvars.go new file mode 100644 index 0000000000..9d185763bc --- /dev/null +++ b/pkg/hostsensormanager/sensor_kernelvars.go @@ -0,0 +1,113 @@ +package hostsensormanager + +import ( + "fmt" + "io" + "os" + "path" + "strings" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +const ( + procSysKernelDir = "/proc/sys/kernel" +) + +// LinuxKernelVariablesSensor implements the Sensor interface for kernel variables data +type LinuxKernelVariablesSensor struct { + nodeName string +} + +// NewLinuxKernelVariablesSensor creates a new kernel variables sensor +func NewLinuxKernelVariablesSensor(nodeName string) *LinuxKernelVariablesSensor { + return &LinuxKernelVariablesSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *LinuxKernelVariablesSensor) GetKind() string { + return "LinuxKernelVariables" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *LinuxKernelVariablesSensor) GetPluralKind() string { + return "linuxkernelvariables" +} + +// Sense collects the kernel variables data from the host +func (s *LinuxKernelVariablesSensor) Sense() (interface{}, error) { + hProcSysKernelDir := hostPath(procSysKernelDir) + procDir, err := os.Open(hProcSysKernelDir) + if err != nil { + return nil, fmt.Errorf("failed to open procSysKernelDir dir(%s): %w", hProcSysKernelDir, err) + } + defer procDir.Close() + + vars, err := s.walkVarsDir(procSysKernelDir, procDir) + if err != nil { + return nil, fmt.Errorf("failed to walk kernel variables: %w", err) + } + + return &LinuxKernelVariablesSpec{ + KernelVariables: vars, + NodeName: s.nodeName, + }, nil +} + +func (s *LinuxKernelVariablesSensor) walkVarsDir(dirPath string, procDir *os.File) ([]KernelVariable, error) { + var varsNames []string + varsList := make([]KernelVariable, 0, 128) + + var err error + for varsNames, err = procDir.Readdirnames(100); err == nil; varsNames, err = procDir.Readdirnames(100) { + for _, varName := range varsNames { + hVarFileName := hostPath(path.Join(dirPath, varName)) + varFile, errOpen := os.Open(hVarFileName) + if errOpen != nil { + if strings.Contains(errOpen.Error(), "permission denied") { + logger.L().Debug("failed to open kernel variable file", helpers.String("path", hVarFileName), helpers.Error(errOpen)) + continue + } + return nil, fmt.Errorf("failed to open file (%s): %w", hVarFileName, errOpen) + } + defer varFile.Close() + + fileInfo, errStat := varFile.Stat() + if errStat != nil { + return nil, fmt.Errorf("failed to stat file (%s): %w", hVarFileName, errStat) + } + + if fileInfo.IsDir() { + // Recursive call + innerVars, errW := s.walkVarsDir(path.Join(dirPath, varName), varFile) + if errW != nil { + return nil, fmt.Errorf("failed to walkVarsDir file (%s): %w", hVarFileName, errW) + } + varsList = append(varsList, innerVars...) + } else if fileInfo.Mode().IsRegular() { + strBld := strings.Builder{} + if _, errCopy := io.Copy(&strBld, varFile); errCopy != nil { + if strings.Contains(errCopy.Error(), "operation not permitted") { + logger.L().Debug("failed to read kernel variable file", helpers.String("path", hVarFileName), helpers.Error(errCopy)) + continue + } + return nil, fmt.Errorf("failed to copy file (%s): %w", hVarFileName, errCopy) + } + varsList = append(varsList, KernelVariable{ + Key: varName, + Value: strBld.String(), + Source: path.Join(dirPath, varName), + }) + } + } + } + + if err != nil && err != io.EOF { + return nil, fmt.Errorf("failed to read directory (%s): %w", dirPath, err) + } + + return varsList, nil +} diff --git a/pkg/hostsensormanager/sensor_kernelversion.go b/pkg/hostsensormanager/sensor_kernelversion.go new file mode 100644 index 0000000000..da0d8e2ab2 --- /dev/null +++ b/pkg/hostsensormanager/sensor_kernelversion.go @@ -0,0 +1,45 @@ +package hostsensormanager + +import ( + "fmt" + "path" +) + +const ( + kernelVersionFileName = "version" +) + +// KernelVersionSensor implements the Sensor interface for kernel version data +type KernelVersionSensor struct { + nodeName string +} + +// NewKernelVersionSensor creates a new kernel version sensor +func NewKernelVersionSensor(nodeName string) *KernelVersionSensor { + return &KernelVersionSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *KernelVersionSensor) GetKind() string { + return "KernelVersion" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *KernelVersionSensor) GetPluralKind() string { + return "kernelversions" +} + +// Sense collects the kernel version data from the host +func (s *KernelVersionSensor) Sense() (interface{}, error) { + content, err := readFileOnHostFileSystem(path.Join(procDirName, kernelVersionFileName)) + if err != nil { + return nil, fmt.Errorf("failed to read kernel version file: %w", err) + } + + return &KernelVersionSpec{ + Content: string(content), + NodeName: s.nodeName, + }, nil +} diff --git a/pkg/hostsensormanager/sensor_kubelet.go b/pkg/hostsensormanager/sensor_kubelet.go new file mode 100644 index 0000000000..d956b198fb --- /dev/null +++ b/pkg/hostsensormanager/sensor_kubelet.go @@ -0,0 +1,83 @@ +package hostsensormanager + +import ( + "context" + "fmt" + + "github.com/kubescape/go-logger/helpers" +) + +const ( + kubeletProcessSuffix = "/kubelet" + kubeletConfigArgName = "--config" + kubeletClientCAArgName = "--client-ca-file" + kubeConfigArgName = "--kubeconfig" +) + +var kubeletConfigDefaultPathList = []string{ + "/var/lib/kubelet/config.yaml", + "/etc/kubernetes/kubelet/kubelet-config.json", +} + +var kubeletKubeConfigDefaultPathList = []string{ + "/etc/kubernetes/kubelet.conf", + "/var/lib/kubelet/kubeconfig", +} + +// KubeletInfoSensor implements the Sensor interface for kubelet info data +type KubeletInfoSensor struct { + nodeName string +} + +// NewKubeletInfoSensor creates a new kubelet info sensor +func NewKubeletInfoSensor(nodeName string) *KubeletInfoSensor { + return &KubeletInfoSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *KubeletInfoSensor) GetKind() string { + return "KubeletInfo" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *KubeletInfoSensor) GetPluralKind() string { + return "kubeletinfos" +} + +// Sense collects the kubelet info data from the host +func (s *KubeletInfoSensor) Sense() (interface{}, error) { + ctx := context.Background() + ret := KubeletInfoSpec{ + NodeName: s.nodeName, + } + + kubeletProcess, err := LocateProcessByExecSuffix(kubeletProcessSuffix) + if err != nil { + return &ret, fmt.Errorf("failed to locate kubelet process: %w", err) + } + + // Config file + if pConfigPath, ok := kubeletProcess.GetArg(kubeletConfigArgName); ok { + ret.ConfigFile = makeContaineredFileInfoVerbose(ctx, kubeletProcess, pConfigPath, true, helpers.String("in", "SenseKubeletInfo")) + } else { + ret.ConfigFile = makeContaineredFileInfoFromListVerbose(ctx, kubeletProcess, kubeletConfigDefaultPathList, true, helpers.String("in", "SenseKubeletInfo")) + } + + // Kubeconfig + if pKubeConfigPath, ok := kubeletProcess.GetArg(kubeConfigArgName); ok { + ret.KubeConfigFile = makeContaineredFileInfoVerbose(ctx, kubeletProcess, pKubeConfigPath, true, helpers.String("in", "SenseKubeletInfo")) + } else { + ret.KubeConfigFile = makeContaineredFileInfoFromListVerbose(ctx, kubeletProcess, kubeletKubeConfigDefaultPathList, true, helpers.String("in", "SenseKubeletInfo")) + } + + // Client CA + if caFilePath, ok := kubeletProcess.GetArg(kubeletClientCAArgName); ok { + ret.ClientCAFile = makeContaineredFileInfoVerbose(ctx, kubeletProcess, caFilePath, false, helpers.String("in", "SenseKubeletInfo")) + } + + ret.CmdLine = kubeletProcess.RawCmd() + + return &ret, nil +} diff --git a/pkg/hostsensormanager/sensor_kubeproxy.go b/pkg/hostsensormanager/sensor_kubeproxy.go new file mode 100644 index 0000000000..449bcc6b2b --- /dev/null +++ b/pkg/hostsensormanager/sensor_kubeproxy.go @@ -0,0 +1,55 @@ +package hostsensormanager + +import ( + "context" + "fmt" + + "github.com/kubescape/go-logger/helpers" +) + +const ( + kubeProxyExe = "kube-proxy" +) + +// KubeProxyInfoSensor implements the Sensor interface for kube-proxy info data +type KubeProxyInfoSensor struct { + nodeName string +} + +// NewKubeProxyInfoSensor creates a new kube-proxy info sensor +func NewKubeProxyInfoSensor(nodeName string) *KubeProxyInfoSensor { + return &KubeProxyInfoSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *KubeProxyInfoSensor) GetKind() string { + return "KubeProxyInfo" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *KubeProxyInfoSensor) GetPluralKind() string { + return "kubeproxyinfos" +} + +// Sense collects the kube-proxy info data from the host +func (s *KubeProxyInfoSensor) Sense() (interface{}, error) { + ctx := context.Background() + ret := KubeProxyInfoSpec{ + NodeName: s.nodeName, + } + + proc, err := LocateProcessByExecSuffix(kubeProxyExe) + if err != nil { + return &ret, fmt.Errorf("failed to locate kube-proxy process: %w", err) + } + + if kubeConfigPath, ok := proc.GetArg(kubeConfigArgName); ok { + ret.KubeConfigFile = makeContaineredFileInfoVerbose(ctx, proc, kubeConfigPath, false, helpers.String("in", "SenseKubeProxyInfo")) + } + + ret.CmdLine = proc.RawCmd() + + return &ret, nil +} diff --git a/pkg/hostsensormanager/sensor_network.go b/pkg/hostsensormanager/sensor_network.go new file mode 100644 index 0000000000..6f9a8d7346 --- /dev/null +++ b/pkg/hostsensormanager/sensor_network.go @@ -0,0 +1,100 @@ +package hostsensormanager + +import ( + "fmt" + "os" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + "github.com/weaveworks/procspy" +) + +const ( + tcpListeningState = 10 +) + +var ( + ProcNetTCPPaths = []string{"/proc/net/tcp", "/proc/net/tcp6"} + ProcNetUDPPaths = []string{"/proc/net/udp", "/proc/net/udp6", "/proc/net/udplite", "/proc/net/udplite6"} + ProcNetICMPPaths = []string{"/proc/net/icmp", "/proc/net/icmp6"} +) + +// OpenPortsSensor implements the Sensor interface for open ports data +type OpenPortsSensor struct { + nodeName string +} + +// NewOpenPortsSensor creates a new open ports sensor +func NewOpenPortsSensor(nodeName string) *OpenPortsSensor { + return &OpenPortsSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *OpenPortsSensor) GetKind() string { + return "OpenPorts" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *OpenPortsSensor) GetPluralKind() string { + return "openports" +} + +// Sense collects the open ports data from the host +func (s *OpenPortsSensor) Sense() (interface{}, error) { + res := &OpenPortsSpec{ + TcpPorts: make([]Connection, 0), + UdpPorts: make([]Connection, 0), + ICMPPorts: make([]Connection, 0), + NodeName: s.nodeName, + } + + // tcp + ports, err := s.getOpenedPorts(ProcNetTCPPaths) + if err != nil { + logger.L().Warning("failed to sense TCP ports", helpers.Error(err)) + } else { + res.TcpPorts = ports + } + + // udp + ports, err = s.getOpenedPorts(ProcNetUDPPaths) + if err != nil { + logger.L().Warning("failed to sense UDP ports", helpers.Error(err)) + } else { + res.UdpPorts = ports + } + + // icmp + ports, err = s.getOpenedPorts(ProcNetICMPPaths) + if err != nil { + logger.L().Warning("failed to sense ICMP ports", helpers.Error(err)) + } else { + res.ICMPPorts = ports + } + + return res, nil +} + +func (s *OpenPortsSensor) getOpenedPorts(pathsList []string) ([]Connection, error) { + res := make([]Connection, 0) + for _, p := range pathsList { + hPath := hostPath(p) + bytesBuf, err := os.ReadFile(hPath) + if err != nil { + return res, fmt.Errorf("failed to ReadFile(%s): %w", hPath, err) + } + netCons := procspy.NewProcNet(bytesBuf, tcpListeningState) + for c := netCons.Next(); c != nil; c = netCons.Next() { + res = append(res, Connection{ + Transport: c.Transport, + LocalAddress: c.LocalAddress.String(), + LocalPort: c.LocalPort, + RemoteAddress: c.RemoteAddress.String(), + RemotePort: c.RemotePort, + }) + } + } + return res, nil +} diff --git a/pkg/hostsensormanager/sensor_osrelease.go b/pkg/hostsensormanager/sensor_osrelease.go new file mode 100644 index 0000000000..5bc2821e58 --- /dev/null +++ b/pkg/hostsensormanager/sensor_osrelease.go @@ -0,0 +1,77 @@ +package hostsensormanager + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +const ( + etcDirName = "/etc" + osReleaseFileSuffix = "os-release" +) + +// OsReleaseSensor implements the Sensor interface for OS release data +type OsReleaseSensor struct { + nodeName string +} + +// NewOsReleaseSensor creates a new OS release sensor +func NewOsReleaseSensor(nodeName string) *OsReleaseSensor { + return &OsReleaseSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *OsReleaseSensor) GetKind() string { + return "OsReleaseFile" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *OsReleaseSensor) GetPluralKind() string { + return "osreleasefiles" +} + +// Sense collects the OS release data from the host +func (s *OsReleaseSensor) Sense() (interface{}, error) { + osFileName, err := s.getOsReleaseFile() + if err != nil { + return nil, fmt.Errorf("failed to find os-release file: %w", err) + } + + content, err := readFileOnHostFileSystem(path.Join(etcDirName, osFileName)) + if err != nil { + return nil, fmt.Errorf("failed to read os-release file: %w", err) + } + + return &OsReleaseFileSpec{ + Content: string(content), + NodeName: s.nodeName, + }, nil +} + +// getOsReleaseFile finds the OS release file in /etc +func (s *OsReleaseSensor) getOsReleaseFile() (string, error) { + hEtcDir := hostPath(etcDirName) + etcDir, err := os.Open(hEtcDir) + if err != nil { + return "", fmt.Errorf("failed to open etc dir: %w", err) + } + defer etcDir.Close() + + var etcSons []string + for etcSons, err = etcDir.Readdirnames(100); err == nil; etcSons, err = etcDir.Readdirnames(100) { + for idx := range etcSons { + if strings.HasSuffix(etcSons[idx], osReleaseFileSuffix) { + logger.L().Debug("os release file found", helpers.String("filename", etcSons[idx])) + return etcSons[idx], nil + } + } + } + return "", fmt.Errorf("os-release file not found in %s", hEtcDir) +} diff --git a/pkg/hostsensormanager/sensor_security.go b/pkg/hostsensormanager/sensor_security.go new file mode 100644 index 0000000000..ac0140301d --- /dev/null +++ b/pkg/hostsensormanager/sensor_security.go @@ -0,0 +1,70 @@ +package hostsensormanager + +import ( + "os" +) + +const ( + appArmorProfilesFileName = "/sys/kernel/security/apparmor/profiles" + seLinuxConfigFileName = "/etc/selinux/semanage.conf" +) + +// LinuxSecurityHardeningSensor implements the Sensor interface for security hardening data +type LinuxSecurityHardeningSensor struct { + nodeName string +} + +// NewLinuxSecurityHardeningSensor creates a new security hardening sensor +func NewLinuxSecurityHardeningSensor(nodeName string) *LinuxSecurityHardeningSensor { + return &LinuxSecurityHardeningSensor{ + nodeName: nodeName, + } +} + +// GetKind returns the CRD kind for this sensor +func (s *LinuxSecurityHardeningSensor) GetKind() string { + return "LinuxSecurityHardening" +} + +// GetPluralKind returns the plural and lowercase form of CRD kind for this sensor +func (s *LinuxSecurityHardeningSensor) GetPluralKind() string { + return "linuxsecurityhardenings" +} + +// Sense collects the security hardening data from the host +func (s *LinuxSecurityHardeningSensor) Sense() (interface{}, error) { + return &LinuxSecurityHardeningSpec{ + AppArmor: s.getAppArmorStatus(), + SeLinux: s.getSELinuxStatus(), + NodeName: s.nodeName, + }, nil +} + +func (s *LinuxSecurityHardeningSensor) getAppArmorStatus() string { + statusStr := "unloaded" + hAppArmorProfilesFileName := hostPath(appArmorProfilesFileName) + profFile, err := os.Open(hAppArmorProfilesFileName) + if err == nil { + defer profFile.Close() + statusStr = "stopped" + content, err := readFileOnHostFileSystem(appArmorProfilesFileName) + if err == nil && len(content) > 0 { + statusStr = string(content) + } + } + return statusStr +} + +func (s *LinuxSecurityHardeningSensor) getSELinuxStatus() string { + statusStr := "not found" + hSELinuxConfigFileName := hostPath(seLinuxConfigFileName) + conFile, err := os.Open(hSELinuxConfigFileName) + if err == nil { + defer conFile.Close() + content, err := readFileOnHostFileSystem(seLinuxConfigFileName) + if err == nil && len(content) > 0 { + statusStr = string(content) + } + } + return statusStr +} diff --git a/pkg/hostsensormanager/sensor_utils.go b/pkg/hostsensormanager/sensor_utils.go new file mode 100644 index 0000000000..b6a4749b02 --- /dev/null +++ b/pkg/hostsensormanager/sensor_utils.go @@ -0,0 +1,238 @@ +package hostsensormanager + +import ( + "bytes" + "context" + "fmt" + "os" + "path" + "strconv" + "strings" + "syscall" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +const ( + procDirName = "/proc" +) + +var hostFSPrefix = "/host_fs" // Mount point for host filesystem + +func init() { + if val := os.Getenv("HOST_ROOT"); val != "" { // use HOST_ROOT as inspektor gadget + hostFSPrefix = val + } +} + +// --- File Utilities --- + +// hostPath converts a path to the host filesystem path +func hostPath(p string) string { + if strings.HasPrefix(p, hostFSPrefix) { + return p + } + return path.Join(hostFSPrefix, p) +} + +// readFileOnHostFileSystem reads a file from the host filesystem +func readFileOnHostFileSystem(filePath string) ([]byte, error) { + hPath := hostPath(filePath) + content, err := os.ReadFile(hPath) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", hPath, err) + } + return content, nil +} + +// MakeFileInfo returns a FileInfo object for given path +func MakeFileInfo(filePath string, readContent bool) (*FileInfo, error) { + ret := FileInfo{Path: filePath} + + // Permissions + info, err := os.Stat(filePath) + if err != nil { + return nil, err + } + ret.Permissions = int(info.Mode().Perm()) + + // Ownership + asUnix, ok := info.Sys().(*syscall.Stat_t) + if !ok { + ret.Ownership = &FileOwnership{Err: "not a unix filesystem"} + } else { + ret.Ownership = &FileOwnership{ + UID: int64(asUnix.Uid), + GID: int64(asUnix.Gid), + } + // Simplified username/groupname - just stringify IDs for now + ret.Ownership.Username = strconv.FormatInt(ret.Ownership.UID, 10) + ret.Ownership.Groupname = strconv.FormatInt(ret.Ownership.GID, 10) + } + + // Content + if readContent { + content, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + ret.Content = content + } + + return &ret, nil +} + +// MakeChangedRootFileInfo makes a file info object for the given path on the given root directory. +func MakeChangedRootFileInfo(rootDir string, filePath string, readContent bool) (*FileInfo, error) { + fullPath := path.Join(rootDir, filePath) + obj, err := MakeFileInfo(fullPath, readContent) + if err != nil { + return obj, err + } + obj.Path = filePath + return obj, nil +} + +// --- Process Utilities --- + +type ProcessDetails struct { + CmdLine []string `json:"cmdline"` + PID int32 `json:"pid"` +} + +func (p ProcessDetails) RootDir() string { + return hostPath(fmt.Sprintf("/proc/%d/root", p.PID)) +} + +func (p ProcessDetails) RawCmd() string { + return strings.Join(p.CmdLine, " ") +} + +func (p ProcessDetails) GetArg(argName string) (string, bool) { + for idx, arg := range p.CmdLine { + if !strings.HasPrefix(arg, argName) { + continue + } + val := arg[len(argName):] + if val != "" { + if strings.HasPrefix(val, "=") { + return val[1:], true + } + continue + } + if idx+1 < len(p.CmdLine) { + return p.CmdLine[idx+1], true + } + return "", true + } + return "", false +} + +// LocateProcessByExecSuffix locates process with executable name ends with processSuffix. +func LocateProcessByExecSuffix(processSuffix string) (*ProcessDetails, error) { + hProcDir := hostPath(procDirName) + procDir, err := os.Open(hProcDir) + if err != nil { + return nil, fmt.Errorf("failed to open processes dir %s: %w", hProcDir, err) + } + defer procDir.Close() + + var pidDirs []string + for pidDirs, err = procDir.Readdirnames(100); err == nil; pidDirs, err = procDir.Readdirnames(100) { + for _, pidDir := range pidDirs { + pid, err := strconv.ParseInt(pidDir, 10, 32) + if err != nil { + continue + } + cmdLinePath := hostPath(path.Join(procDirName, pidDir, "cmdline")) + cmdLine, err := os.ReadFile(cmdLinePath) + if err != nil { + continue + } + cmdLineSplitted := bytes.Split(cmdLine, []byte{00}) + if len(cmdLineSplitted) == 0 || len(cmdLineSplitted[0]) == 0 { + continue + } + processName := cmdLineSplitted[0] + if processName[0] != '/' && processName[0] != '[' { + processName = append([]byte{'/'}, processName...) + } + if bytes.HasSuffix(processName, []byte(processSuffix)) { + res := &ProcessDetails{PID: int32(pid), CmdLine: make([]string, 0, len(cmdLineSplitted))} + for _, part := range cmdLineSplitted { + if len(part) > 0 { + res.CmdLine = append(res.CmdLine, string(part)) + } + } + return res, nil + } + } + } + return nil, fmt.Errorf("process with suffix %s not found", processSuffix) +} + +// --- Verbose Helpers --- + +func makeHostFileInfoVerbose(ctx context.Context, filePath string, readContent bool, failMsgs ...helpers.IDetails) *FileInfo { + fileInfo, err := MakeChangedRootFileInfo(hostFSPrefix, filePath, readContent) + if err != nil { + logArgs := append([]helpers.IDetails{helpers.String("path", filePath), helpers.Error(err)}, failMsgs...) + logger.L().Ctx(ctx).Debug("failed to MakeHostFileInfo", logArgs...) + } + return fileInfo +} + +func makeContaineredFileInfoVerbose(ctx context.Context, p *ProcessDetails, filePath string, readContent bool, failMsgs ...helpers.IDetails) *FileInfo { + fileInfo, err := MakeChangedRootFileInfo(p.RootDir(), filePath, readContent) + if err != nil { + logArgs := append([]helpers.IDetails{helpers.String("path", filePath), helpers.Error(err)}, failMsgs...) + logger.L().Ctx(ctx).Debug("failed to makeContaineredFileInfo", logArgs...) + } + return fileInfo +} + +func makeContaineredFileInfoFromListVerbose(ctx context.Context, p *ProcessDetails, filePathList []string, readContent bool, failMsgs ...helpers.IDetails) *FileInfo { + for _, filePath := range filePathList { + fileInfo := makeContaineredFileInfoVerbose(ctx, p, filePath, readContent, failMsgs...) + if fileInfo != nil { + return fileInfo + } + } + return nil +} + +func makeHostDirFilesInfoVerbose(ctx context.Context, dir string, recursive bool, recursionLevel int) ([]*FileInfo, error) { + if recursionLevel > 5 { // Limit recursion + return nil, nil + } + hDirPath := hostPath(dir) + dirInfo, err := os.Open(hDirPath) + if err != nil { + return nil, fmt.Errorf("failed to open dir %s: %w", hDirPath, err) + } + defer dirInfo.Close() + + var fileInfos []*FileInfo + var fileNames []string + for fileNames, err = dirInfo.Readdirnames(100); err == nil; fileNames, err = dirInfo.Readdirnames(100) { + for _, fileName := range fileNames { + filePath := path.Join(dir, fileName) + hFilePath := hostPath(filePath) + stats, err := os.Stat(hFilePath) + if err != nil { + continue + } + if stats.IsDir() && recursive { + innerInfos, _ := makeHostDirFilesInfoVerbose(ctx, filePath, recursive, recursionLevel+1) + fileInfos = append(fileInfos, innerInfos...) + } else if !stats.IsDir() { + info := makeHostFileInfoVerbose(ctx, filePath, false) + if info != nil { + fileInfos = append(fileInfos, info) + } + } + } + } + return fileInfos, nil +} diff --git a/pkg/hostsensormanager/types.go b/pkg/hostsensormanager/types.go new file mode 100644 index 0000000000..c9a08e5c81 --- /dev/null +++ b/pkg/hostsensormanager/types.go @@ -0,0 +1,269 @@ +package hostsensormanager + +import ( + "context" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // API group and version for host data CRDs + HostDataGroup = "hostdata.kubescape.cloud" + HostDataVersion = "v1beta1" +) + +// HostSensorManager manages the lifecycle of host sensors +type HostSensorManager interface { + // Start begins the sensing loop + Start(ctx context.Context) error + // Stop gracefully stops the manager + Stop() error +} + +// Sensor represents a single host sensor that can collect data +type Sensor interface { + // Sense collects the data from the host + Sense() (interface{}, error) + // GetKind returns the CRD kind for this sensor + GetKind() string + // GetPluralKind returns the plural and lowercase form of CRD kind for this sensor + GetPluralKind() string +} + +// Status contains status information about the sensing (common for all host data CRDs) +type Status struct { + LastSensed metav1.Time `json:"lastSensed,omitempty"` + Error string `json:"error,omitempty"` +} + +// FileInfo holds information about a file +type FileInfo struct { + Ownership *FileOwnership `json:"ownership"` + Path string `json:"path"` + Content []byte `json:"content,omitempty"` + Permissions int `json:"permissions"` +} + +// FileOwnership holds the ownership of a file +type FileOwnership struct { + Err string `json:"err,omitempty"` + UID int64 `json:"uid"` + GID int64 `json:"gid"` + Username string `json:"username"` + Groupname string `json:"groupname"` +} + +// KernelVariable represents a single kernel variable +type KernelVariable struct { + Key string `json:"key"` + Value string `json:"value"` + Source string `json:"source"` +} + +// Connection represents a network connection (minimal version of procspy.Connection) +type Connection struct { + Transport string `json:"transport"` + LocalAddress string `json:"localAddress"` + LocalPort uint16 `json:"localPort"` + RemoteAddress string `json:"remoteAddress"` + RemotePort uint16 `json:"remotePort"` +} + +// --- OsReleaseFile --- + +// OsReleaseFile represents the CRD structure for OS release data +type OsReleaseFile struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OsReleaseFileSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +// OsReleaseFileSpec contains the actual OS release file content +type OsReleaseFileSpec struct { + Content string `json:"content"` + NodeName string `json:"nodeName"` +} + +// --- KernelVersion --- + +// KernelVersion represents the CRD structure for kernel version data +type KernelVersion struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KernelVersionSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +type KernelVersionSpec struct { + Content string `json:"content"` + NodeName string `json:"nodeName"` +} + +// --- LinuxSecurityHardening --- + +// LinuxSecurityHardening represents the CRD structure for security hardening data +type LinuxSecurityHardening struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec LinuxSecurityHardeningSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +type LinuxSecurityHardeningSpec struct { + AppArmor string `json:"appArmor"` + SeLinux string `json:"seLinux"` + NodeName string `json:"nodeName"` +} + +// --- OpenPorts --- + +// OpenPorts represents the CRD structure for open ports data +type OpenPorts struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OpenPortsSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +type OpenPortsSpec struct { + TcpPorts []Connection `json:"tcpPorts"` + UdpPorts []Connection `json:"udpPorts"` + ICMPPorts []Connection `json:"icmpPorts"` + NodeName string `json:"nodeName"` +} + +// --- LinuxKernelVariables --- + +// LinuxKernelVariables represents the CRD structure for kernel variables data +type LinuxKernelVariables struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec LinuxKernelVariablesSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +type LinuxKernelVariablesSpec struct { + KernelVariables []KernelVariable `json:"kernelVariables"` + NodeName string `json:"nodeName"` +} + +// --- KubeletInfo --- + +// KubeletInfo represents the CRD structure for kubelet info data +type KubeletInfo struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KubeletInfoSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +type KubeletInfoSpec struct { + ServiceFiles []FileInfo `json:"serviceFiles,omitempty"` + ConfigFile *FileInfo `json:"configFile,omitempty"` + KubeConfigFile *FileInfo `json:"kubeConfigFile,omitempty"` + ClientCAFile *FileInfo `json:"clientCAFile,omitempty"` + CmdLine string `json:"cmdLine"` + NodeName string `json:"nodeName"` +} + +// --- KubeProxyInfo --- + +// KubeProxyInfo represents the CRD structure for kube-proxy info data +type KubeProxyInfo struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KubeProxyInfoSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +type KubeProxyInfoSpec struct { + KubeConfigFile *FileInfo `json:"kubeConfigFile,omitempty"` + CmdLine string `json:"cmdLine"` + NodeName string `json:"nodeName"` +} + +// --- ControlPlaneInfo --- + +// ControlPlaneInfo represents the CRD structure for control plane info data +type ControlPlaneInfo struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ControlPlaneInfoSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +type ControlPlaneInfoSpec struct { + APIServerInfo *ApiServerInfo `json:"APIServerInfo,omitempty"` + ControllerManagerInfo *ProcessInfo `json:"controllerManagerInfo,omitempty"` + SchedulerInfo *ProcessInfo `json:"schedulerInfo,omitempty"` + EtcdConfigFile *FileInfo `json:"etcdConfigFile,omitempty"` + EtcdDataDir *FileInfo `json:"etcdDataDir,omitempty"` + AdminConfigFile *FileInfo `json:"adminConfigFile,omitempty"` + PKIDir *FileInfo `json:"PKIDir,omitempty"` + PKIFiles []*FileInfo `json:"PKIFiles,omitempty"` + NodeName string `json:"nodeName"` +} + +type ProcessInfo struct { + SpecsFile *FileInfo `json:"specsFile,omitempty"` + ConfigFile *FileInfo `json:"configFile,omitempty"` + KubeConfigFile *FileInfo `json:"kubeConfigFile,omitempty"` + ClientCAFile *FileInfo `json:"clientCAFile,omitempty"` + CmdLine string `json:"cmdLine"` +} + +type ApiServerInfo struct { + EncryptionProviderConfigFile *FileInfo `json:"encryptionProviderConfigFile,omitempty"` + AuditPolicyFile *FileInfo `json:"auditPolicyFile,omitempty"` + ProcessInfo `json:",inline"` +} + +// --- CloudProviderInfo --- + +// CloudProviderInfo represents the CRD structure for cloud provider info data +type CloudProviderInfo struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CloudProviderInfoSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +type CloudProviderInfoSpec struct { + ProviderMetaDataAPIAccess bool `json:"providerMetaDataAPIAccess"` + NodeName string `json:"nodeName"` +} + +// --- CNIInfo --- + +// CNIInfo represents the CRD structure for CNI info data +type CNIInfo struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CNIInfoSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` +} + +type CNIInfoSpec struct { + CNIConfigFiles []*FileInfo `json:"CNIConfigFiles,omitempty"` + CNINames []string `json:"CNINames,omitempty"` + NodeName string `json:"nodeName"` +} + +// Config holds the configuration for the host sensor manager +type Config struct { + Enabled bool + Interval time.Duration + NodeName string +}