From c525d3ab6069e868071d75a9161660fd38d5e285 Mon Sep 17 00:00:00 2001 From: Bezalel Brandwine Date: Wed, 31 Dec 2025 21:06:32 +0200 Subject: [PATCH 1/6] OSRelease Signed-off-by: Bezalel Brandwine --- cmd/main.go | 18 +++ deploy/crds/osreleasefile-crd.yaml | 62 ++++++++ pkg/config/config.go | 7 + pkg/hostsensormanager/crd_client.go | 150 +++++++++++++++++++ pkg/hostsensormanager/manager.go | 175 ++++++++++++++++++++++ pkg/hostsensormanager/sensor_osrelease.go | 88 +++++++++++ pkg/hostsensormanager/types.go | 65 ++++++++ 7 files changed, 565 insertions(+) create mode 100644 deploy/crds/osreleasefile-crd.yaml create mode 100644 pkg/hostsensormanager/crd_client.go create mode 100644 pkg/hostsensormanager/manager.go create mode 100644 pkg/hostsensormanager/sensor_osrelease.go create mode 100644 pkg/hostsensormanager/types.go diff --git a/cmd/main.go b/cmd/main.go index 39c7c818c4..4aa0a42cb7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -29,6 +29,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" @@ -175,6 +176,17 @@ func main() { } dWatcher.AddAdaptor(k8sObjectCache) + // Create the host sensor manager + 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)) + } + // Create the seccomp manager var seccompManager seccompmanager.SeccompManagerClient if cfg.EnableSeccomp { @@ -372,6 +384,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/deploy/crds/osreleasefile-crd.yaml b/deploy/crds/osreleasefile-crd.yaml new file mode 100644 index 0000000000..fbc2737f84 --- /dev/null +++ b/deploy/crds/osreleasefile-crd.yaml @@ -0,0 +1,62 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: osreleasefiles.hostdata.kubescape.cloud + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 +spec: + group: hostdata.kubescape.cloud + names: + kind: OsReleaseFile + listKind: OsReleaseFileList + plural: osreleasefiles + singular: osreleasefile + scope: Cluster + versions: + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + description: OsReleaseFile contains the OS release information from a node + type: object + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object.' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents.' + type: string + metadata: + type: object + spec: + description: OsReleaseFileSpec contains the actual OS release file content + type: object + properties: + content: + description: Content is the raw content of the OS release file + type: string + nodeName: + description: NodeName is the name of the node this data came from + type: string + status: + description: OsReleaseFileStatus contains status information about the sensing + type: object + properties: + lastSensed: + description: LastSensed is the timestamp when this data was last collected + type: string + format: date-time + error: + description: Error contains any error message from the last sensing attempt + type: string + additionalPrinterColumns: + - name: Node + type: string + jsonPath: .spec.nodeName + - name: Last Sensed + type: string + jsonPath: .status.lastSensed + - name: Age + type: date + jsonPath: .metadata.creationTimestamp diff --git a/pkg/config/config.go b/pkg/config/config.go index e69afccc4c..3d0218eae0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -83,6 +83,10 @@ type Config struct { DSymlink bool `mapstructure:"dSymlink"` DTop bool `mapstructure:"dTop"` FIM FIMConfig `mapstructure:"fim"` + + // Host sensor configuration + EnableHostSensor bool `mapstructure:"hostSensorEnabled"` + HostSensorInterval time.Duration `mapstructure:"hostSensorInterval"` } // FIMConfig defines the configuration for File Integrity Monitoring @@ -179,6 +183,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/hostsensormanager/crd_client.go b/pkg/hostsensormanager/crd_client.go new file mode 100644 index 0000000000..21b62ef3f6 --- /dev/null +++ b/pkg/hostsensormanager/crd_client.go @@ -0,0 +1,150 @@ +package hostsensormanager + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + 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 +} + +// CreateOrUpdateOsReleaseFile creates or updates an OsReleaseFile CRD +func (c *CRDClient) CreateOrUpdateOsReleaseFile(ctx context.Context, spec *OsReleaseFileSpec) error { + gvr := schema.GroupVersionResource{ + Group: HostDataGroup, + Version: HostDataVersion, + Resource: "osreleasefiles", + } + + // Create the CRD object + osReleaseFile := &OsReleaseFile{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", HostDataGroup, HostDataVersion), + Kind: "OsReleaseFile", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: c.nodeName, + }, + Spec: *spec, + Status: OsReleaseFileStatus{ + LastSensed: metav1.Now(), + }, + } + + // Convert to unstructured + unstructuredObj, err := toUnstructured(osReleaseFile) + if err != nil { + return fmt.Errorf("failed to convert to unstructured: %w", err) + } + + // Try to get existing resource + existing, err := c.dynamicClient.Resource(gvr).Get(ctx, c.nodeName, metav1.GetOptions{}) + if err != nil { + // Resource doesn't exist, create it + logger.L().Debug("creating new OsReleaseFile CRD", helpers.String("nodeName", c.nodeName)) + _, err = c.dynamicClient.Resource(gvr).Create(ctx, unstructuredObj, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create OsReleaseFile CRD: %w", err) + } + logger.L().Info("created OsReleaseFile CRD", helpers.String("nodeName", c.nodeName)) + return nil + } + + // Resource exists, update it using patch + logger.L().Debug("updating existing OsReleaseFile CRD", helpers.String("nodeName", c.nodeName)) + + // Preserve the resource version + unstructuredObj.SetResourceVersion(existing.GetResourceVersion()) + + // Create patch data + patchData, err := json.Marshal(map[string]interface{}{ + "spec": spec, + "status": OsReleaseFileStatus{ + LastSensed: metav1.Now(), + }, + }) + 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 patch OsReleaseFile CRD: %w", err) + } + + logger.L().Debug("updated OsReleaseFile CRD", helpers.String("nodeName", c.nodeName)) + return nil +} + +// UpdateStatus updates the status of an OsReleaseFile CRD with an error +func (c *CRDClient) UpdateStatus(ctx context.Context, errorMsg string) error { + gvr := schema.GroupVersionResource{ + Group: HostDataGroup, + Version: HostDataVersion, + Resource: "osreleasefiles", + } + + patchData, err := json.Marshal(map[string]interface{}{ + "status": OsReleaseFileStatus{ + 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..129d2cb2de --- /dev/null +++ b/pkg/hostsensormanager/manager.go @@ -0,0 +1,175 @@ +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 + mu sync.Mutex + running bool +} + +// NewHostSensorManager creates a new host sensor manager +func NewHostSensorManager(config Config) (HostSensorManager, error) { + if !config.Enabled { + logger.L().Info("host sensor manager is disabled") + return &noopManager{}, 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), + } + + 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.mu.Lock() + if m.running { + m.mu.Unlock() + return fmt.Errorf("manager is already running") + } + m.running = true + m.mu.Unlock() + + 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 { + m.mu.Lock() + if !m.running { + m.mu.Unlock() + return nil + } + m.running = false + m.mu.Unlock() + + logger.L().Info("stopping host sensor manager") + 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())) + + // Sense the data + data, err := sensor.Sense() + if err != nil { + // Update status with error + if updateErr := m.crdClient.UpdateStatus(ctx, 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 based on sensor type + switch sensor.GetKind() { + case "OsReleaseFile": + spec, ok := data.(*OsReleaseFileSpec) + if !ok { + return fmt.Errorf("invalid data type for OsReleaseFile") + } + if err := m.crdClient.CreateOrUpdateOsReleaseFile(ctx, spec); err != nil { + return fmt.Errorf("failed to create/update CRD: %w", err) + } + default: + return fmt.Errorf("unknown sensor kind: %s", sensor.GetKind()) + } + + 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{} + +func (n *noopManager) Start(ctx context.Context) error { + return nil +} + +func (n *noopManager) Stop() error { + return nil +} diff --git a/pkg/hostsensormanager/sensor_osrelease.go b/pkg/hostsensormanager/sensor_osrelease.go new file mode 100644 index 0000000000..1085795c67 --- /dev/null +++ b/pkg/hostsensormanager/sensor_osrelease.go @@ -0,0 +1,88 @@ +package hostsensormanager + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +const ( + etcDirName = "/etc" + osReleaseFileSuffix = "os-release" + hostFSPrefix = "/host_fs" // Mount point for host filesystem +) + +// 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" +} + +// 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 := s.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) { + hostEtcDir := s.hostPath(etcDirName) + etcDir, err := os.Open(hostEtcDir) + 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", hostEtcDir) +} + +// hostPath converts a path to the host filesystem path +func (s *OsReleaseSensor) hostPath(p string) string { + return path.Join(hostFSPrefix, p) +} + +// readFileOnHostFileSystem reads a file from the host filesystem +func (s *OsReleaseSensor) readFileOnHostFileSystem(filePath string) ([]byte, error) { + hostPath := s.hostPath(filePath) + content, err := os.ReadFile(hostPath) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", hostPath, err) + } + return content, nil +} diff --git a/pkg/hostsensormanager/types.go b/pkg/hostsensormanager/types.go new file mode 100644 index 0000000000..ec47e0fd83 --- /dev/null +++ b/pkg/hostsensormanager/types.go @@ -0,0 +1,65 @@ +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 +} + +// 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 OsReleaseFileStatus `json:"status,omitempty"` +} + +// OsReleaseFileSpec contains the actual OS release file content +type OsReleaseFileSpec struct { + Content string `json:"content"` + NodeName string `json:"nodeName"` +} + +// OsReleaseFileStatus contains status information about the sensing +type OsReleaseFileStatus struct { + LastSensed metav1.Time `json:"lastSensed,omitempty"` + Error string `json:"error,omitempty"` +} + +// OsReleaseFileList contains a list of OsReleaseFile +type OsReleaseFileList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OsReleaseFile `json:"items"` +} + +// Config holds the configuration for the host sensor manager +type Config struct { + Enabled bool + Interval time.Duration + NodeName string +} From 03eaf6934e197414d4e6e315f92a70c03f04e44a Mon Sep 17 00:00:00 2001 From: Bezalel Brandwine Date: Wed, 31 Dec 2025 22:22:18 +0200 Subject: [PATCH 2/6] add the rest of the sensors Signed-off-by: Bezalel Brandwine --- go.mod | 1 + go.sum | 2 + pkg/hostsensormanager/crd_client.go | 72 +++--- pkg/hostsensormanager/manager.go | 53 +++- pkg/hostsensormanager/sensor_cloudprovider.go | 79 ++++++ pkg/hostsensormanager/sensor_cni.go | 76 ++++++ pkg/hostsensormanager/sensor_controlplane.go | 84 +++++++ pkg/hostsensormanager/sensor_kernelvars.go | 108 ++++++++ pkg/hostsensormanager/sensor_kernelversion.go | 40 +++ pkg/hostsensormanager/sensor_kubelet.go | 78 ++++++ pkg/hostsensormanager/sensor_kubeproxy.go | 50 ++++ pkg/hostsensormanager/sensor_network.go | 95 +++++++ pkg/hostsensormanager/sensor_osrelease.go | 24 +- pkg/hostsensormanager/sensor_security.go | 65 +++++ pkg/hostsensormanager/sensor_utils.go | 231 ++++++++++++++++++ pkg/hostsensormanager/types.go | 224 ++++++++++++++++- 16 files changed, 1202 insertions(+), 80 deletions(-) create mode 100644 pkg/hostsensormanager/sensor_cloudprovider.go create mode 100644 pkg/hostsensormanager/sensor_cni.go create mode 100644 pkg/hostsensormanager/sensor_controlplane.go create mode 100644 pkg/hostsensormanager/sensor_kernelvars.go create mode 100644 pkg/hostsensormanager/sensor_kernelversion.go create mode 100644 pkg/hostsensormanager/sensor_kubelet.go create mode 100644 pkg/hostsensormanager/sensor_kubeproxy.go create mode 100644 pkg/hostsensormanager/sensor_network.go create mode 100644 pkg/hostsensormanager/sensor_security.go create mode 100644 pkg/hostsensormanager/sensor_utils.go diff --git a/go.mod b/go.mod index ce8f358457..ddc6288e17 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,7 @@ require ( github.com/spf13/afero v1.15.0 github.com/spf13/viper v1.20.1 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 7987a8f68f..4186bc124a 100644 --- a/go.sum +++ b/go.sum @@ -1817,6 +1817,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/hostsensormanager/crd_client.go b/pkg/hostsensormanager/crd_client.go index 21b62ef3f6..22a3f1af5d 100644 --- a/pkg/hostsensormanager/crd_client.go +++ b/pkg/hostsensormanager/crd_client.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" @@ -39,58 +40,55 @@ func NewCRDClient(nodeName string) (*CRDClient, error) { }, nil } -// CreateOrUpdateOsReleaseFile creates or updates an OsReleaseFile CRD -func (c *CRDClient) CreateOrUpdateOsReleaseFile(ctx context.Context, spec *OsReleaseFileSpec) error { +// 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: "osreleasefiles", + Resource: resource, } - // Create the CRD object - osReleaseFile := &OsReleaseFile{ - TypeMeta: metav1.TypeMeta{ - APIVersion: fmt.Sprintf("%s/%s", HostDataGroup, HostDataVersion), - Kind: "OsReleaseFile", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: c.nodeName, - }, - Spec: *spec, - Status: OsReleaseFileStatus{ - LastSensed: metav1.Now(), + // 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), + }, }, } - // Convert to unstructured - unstructuredObj, err := toUnstructured(osReleaseFile) - if err != nil { - return fmt.Errorf("failed to convert to unstructured: %w", err) - } - // Try to get existing resource - existing, err := c.dynamicClient.Resource(gvr).Get(ctx, c.nodeName, metav1.GetOptions{}) + _, err := c.dynamicClient.Resource(gvr).Get(ctx, c.nodeName, metav1.GetOptions{}) if err != nil { // Resource doesn't exist, create it - logger.L().Debug("creating new OsReleaseFile CRD", helpers.String("nodeName", c.nodeName)) + logger.L().Debug("creating new host data CRD", + helpers.String("kind", kind), + helpers.String("nodeName", c.nodeName)) _, err = c.dynamicClient.Resource(gvr).Create(ctx, unstructuredObj, metav1.CreateOptions{}) if err != nil { - return fmt.Errorf("failed to create OsReleaseFile CRD: %w", err) + return fmt.Errorf("failed to create %s CRD: %w", kind, err) } - logger.L().Info("created OsReleaseFile CRD", helpers.String("nodeName", c.nodeName)) + logger.L().Info("created host data CRD", + helpers.String("kind", kind), + helpers.String("nodeName", c.nodeName)) return nil } // Resource exists, update it using patch - logger.L().Debug("updating existing OsReleaseFile CRD", helpers.String("nodeName", c.nodeName)) - - // Preserve the resource version - unstructuredObj.SetResourceVersion(existing.GetResourceVersion()) + logger.L().Debug("updating existing host data CRD", + helpers.String("kind", kind), + helpers.String("nodeName", c.nodeName)) // Create patch data patchData, err := json.Marshal(map[string]interface{}{ "spec": spec, - "status": OsReleaseFileStatus{ + "status": Status{ LastSensed: metav1.Now(), }, }) @@ -100,23 +98,25 @@ func (c *CRDClient) CreateOrUpdateOsReleaseFile(ctx context.Context, spec *OsRel _, err = c.dynamicClient.Resource(gvr).Patch(ctx, c.nodeName, types.MergePatchType, patchData, metav1.PatchOptions{}) if err != nil { - return fmt.Errorf("failed to patch OsReleaseFile CRD: %w", err) + return fmt.Errorf("failed to patch %s CRD: %w", kind, err) } - logger.L().Debug("updated OsReleaseFile CRD", helpers.String("nodeName", c.nodeName)) + logger.L().Debug("updated host data CRD", + helpers.String("kind", kind), + helpers.String("nodeName", c.nodeName)) return nil } -// UpdateStatus updates the status of an OsReleaseFile CRD with an error -func (c *CRDClient) UpdateStatus(ctx context.Context, errorMsg string) error { +// 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: "osreleasefiles", + Resource: resource, } patchData, err := json.Marshal(map[string]interface{}{ - "status": OsReleaseFileStatus{ + "status": Status{ LastSensed: metav1.Now(), Error: errorMsg, }, diff --git a/pkg/hostsensormanager/manager.go b/pkg/hostsensormanager/manager.go index 129d2cb2de..1a6d24d856 100644 --- a/pkg/hostsensormanager/manager.go +++ b/pkg/hostsensormanager/manager.go @@ -44,6 +44,15 @@ func NewHostSensorManager(config Config) (HostSensorManager, error) { // 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{ @@ -133,11 +142,38 @@ func (m *manager) runSensing(ctx context.Context) { 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 := "" + switch sensor.GetKind() { + case "OsReleaseFile": + resource = "osreleasefiles" + case "KernelVersion": + resource = "kernelversions" + case "LinuxSecurityHardening": + resource = "linuxsecurityhardenings" + case "OpenPorts": + resource = "openports" + case "LinuxKernelVariables": + resource = "linuxkernelvariables" + case "KubeletInfo": + resource = "kubeletinfos" + case "KubeProxyInfo": + resource = "kubeproxyinfos" + case "ControlPlaneInfo": + resource = "controlplaneinfos" + case "CloudProviderInfo": + resource = "cloudproviderinfos" + case "CNIInfo": + resource = "cniinfos" + default: + return fmt.Errorf("unknown sensor kind: %s", sensor.GetKind()) + } + // Sense the data data, err := sensor.Sense() if err != nil { // Update status with error - if updateErr := m.crdClient.UpdateStatus(ctx, err.Error()); updateErr != nil { + 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)) @@ -145,18 +181,9 @@ func (m *manager) runSensor(ctx context.Context, sensor Sensor) error { return fmt.Errorf("failed to sense data: %w", err) } - // Update CRD based on sensor type - switch sensor.GetKind() { - case "OsReleaseFile": - spec, ok := data.(*OsReleaseFileSpec) - if !ok { - return fmt.Errorf("invalid data type for OsReleaseFile") - } - if err := m.crdClient.CreateOrUpdateOsReleaseFile(ctx, spec); err != nil { - return fmt.Errorf("failed to create/update CRD: %w", err) - } - default: - return fmt.Errorf("unknown sensor kind: %s", sensor.GetKind()) + // 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())) diff --git a/pkg/hostsensormanager/sensor_cloudprovider.go b/pkg/hostsensormanager/sensor_cloudprovider.go new file mode 100644 index 0000000000..aca55271ef --- /dev/null +++ b/pkg/hostsensormanager/sensor_cloudprovider.go @@ -0,0 +1,79 @@ +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" +} + +// 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..571b42ddff --- /dev/null +++ b/pkg/hostsensormanager/sensor_cni.go @@ -0,0 +1,76 @@ +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" +} + +// 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..cbae078642 --- /dev/null +++ b/pkg/hostsensormanager/sensor_controlplane.go @@ -0,0 +1,84 @@ +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" +} + +// 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..443ab0231d --- /dev/null +++ b/pkg/hostsensormanager/sensor_kernelvars.go @@ -0,0 +1,108 @@ +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" +} + +// 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..30be3f1a2c --- /dev/null +++ b/pkg/hostsensormanager/sensor_kernelversion.go @@ -0,0 +1,40 @@ +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" +} + +// 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..fead793395 --- /dev/null +++ b/pkg/hostsensormanager/sensor_kubelet.go @@ -0,0 +1,78 @@ +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" +} + +// 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..106f64cd71 --- /dev/null +++ b/pkg/hostsensormanager/sensor_kubeproxy.go @@ -0,0 +1,50 @@ +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" +} + +// 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..a212a2a1eb --- /dev/null +++ b/pkg/hostsensormanager/sensor_network.go @@ -0,0 +1,95 @@ +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" +} + +// 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 index 1085795c67..df25f6bf74 100644 --- a/pkg/hostsensormanager/sensor_osrelease.go +++ b/pkg/hostsensormanager/sensor_osrelease.go @@ -13,7 +13,6 @@ import ( const ( etcDirName = "/etc" osReleaseFileSuffix = "os-release" - hostFSPrefix = "/host_fs" // Mount point for host filesystem ) // OsReleaseSensor implements the Sensor interface for OS release data @@ -40,7 +39,7 @@ func (s *OsReleaseSensor) Sense() (interface{}, error) { return nil, fmt.Errorf("failed to find os-release file: %w", err) } - content, err := s.readFileOnHostFileSystem(path.Join(etcDirName, osFileName)) + content, err := readFileOnHostFileSystem(path.Join(etcDirName, osFileName)) if err != nil { return nil, fmt.Errorf("failed to read os-release file: %w", err) } @@ -53,8 +52,8 @@ func (s *OsReleaseSensor) Sense() (interface{}, error) { // getOsReleaseFile finds the OS release file in /etc func (s *OsReleaseSensor) getOsReleaseFile() (string, error) { - hostEtcDir := s.hostPath(etcDirName) - etcDir, err := os.Open(hostEtcDir) + hEtcDir := hostPath(etcDirName) + etcDir, err := os.Open(hEtcDir) if err != nil { return "", fmt.Errorf("failed to open etc dir: %w", err) } @@ -69,20 +68,5 @@ func (s *OsReleaseSensor) getOsReleaseFile() (string, error) { } } } - return "", fmt.Errorf("os-release file not found in %s", hostEtcDir) -} - -// hostPath converts a path to the host filesystem path -func (s *OsReleaseSensor) hostPath(p string) string { - return path.Join(hostFSPrefix, p) -} - -// readFileOnHostFileSystem reads a file from the host filesystem -func (s *OsReleaseSensor) readFileOnHostFileSystem(filePath string) ([]byte, error) { - hostPath := s.hostPath(filePath) - content, err := os.ReadFile(hostPath) - if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", hostPath, err) - } - return content, 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..12e65d5c12 --- /dev/null +++ b/pkg/hostsensormanager/sensor_security.go @@ -0,0 +1,65 @@ +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" +} + +// 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..37798863ca --- /dev/null +++ b/pkg/hostsensormanager/sensor_utils.go @@ -0,0 +1,231 @@ +package hostsensormanager + +import ( + "bytes" + "context" + "fmt" + "os" + "path" + "strconv" + "strings" + "syscall" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +const ( + hostFSPrefix = "/host_fs" // Mount point for host filesystem + procDirName = "/proc" +) + +// --- 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 index ec47e0fd83..40a8f1a09b 100644 --- a/pkg/hostsensormanager/types.go +++ b/pkg/hostsensormanager/types.go @@ -29,13 +29,54 @@ type Sensor interface { GetKind() 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 OsReleaseFileStatus `json:"status,omitempty"` + Spec OsReleaseFileSpec `json:"spec,omitempty"` + Status Status `json:"status,omitempty"` } // OsReleaseFileSpec contains the actual OS release file content @@ -44,17 +85,178 @@ type OsReleaseFileSpec struct { NodeName string `json:"nodeName"` } -// OsReleaseFileStatus contains status information about the sensing -type OsReleaseFileStatus struct { - LastSensed metav1.Time `json:"lastSensed,omitempty"` - Error string `json:"error,omitempty"` +// --- 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"` } -// OsReleaseFileList contains a list of OsReleaseFile -type OsReleaseFileList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []OsReleaseFile `json:"items"` +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 From 07c019091d25aa339ad5cfc6dbf573404e931624 Mon Sep 17 00:00:00 2001 From: Bezalel Brandwine Date: Thu, 1 Jan 2026 15:18:28 +0200 Subject: [PATCH 3/6] GetPluralKind Signed-off-by: Bezalel Brandwine --- pkg/hostsensormanager/manager.go | 26 +------------------ pkg/hostsensormanager/sensor_cloudprovider.go | 5 ++++ pkg/hostsensormanager/sensor_cni.go | 5 ++++ pkg/hostsensormanager/sensor_controlplane.go | 5 ++++ pkg/hostsensormanager/sensor_kernelvars.go | 5 ++++ pkg/hostsensormanager/sensor_kernelversion.go | 5 ++++ pkg/hostsensormanager/sensor_kubelet.go | 5 ++++ pkg/hostsensormanager/sensor_kubeproxy.go | 5 ++++ pkg/hostsensormanager/sensor_network.go | 5 ++++ pkg/hostsensormanager/sensor_osrelease.go | 5 ++++ pkg/hostsensormanager/sensor_security.go | 5 ++++ pkg/hostsensormanager/types.go | 2 ++ 12 files changed, 53 insertions(+), 25 deletions(-) diff --git a/pkg/hostsensormanager/manager.go b/pkg/hostsensormanager/manager.go index 1a6d24d856..6dd275b208 100644 --- a/pkg/hostsensormanager/manager.go +++ b/pkg/hostsensormanager/manager.go @@ -143,31 +143,7 @@ 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 := "" - switch sensor.GetKind() { - case "OsReleaseFile": - resource = "osreleasefiles" - case "KernelVersion": - resource = "kernelversions" - case "LinuxSecurityHardening": - resource = "linuxsecurityhardenings" - case "OpenPorts": - resource = "openports" - case "LinuxKernelVariables": - resource = "linuxkernelvariables" - case "KubeletInfo": - resource = "kubeletinfos" - case "KubeProxyInfo": - resource = "kubeproxyinfos" - case "ControlPlaneInfo": - resource = "controlplaneinfos" - case "CloudProviderInfo": - resource = "cloudproviderinfos" - case "CNIInfo": - resource = "cniinfos" - default: - return fmt.Errorf("unknown sensor kind: %s", sensor.GetKind()) - } + resource := sensor.GetPluralKind() // Sense the data data, err := sensor.Sense() diff --git a/pkg/hostsensormanager/sensor_cloudprovider.go b/pkg/hostsensormanager/sensor_cloudprovider.go index aca55271ef..f6aad45727 100644 --- a/pkg/hostsensormanager/sensor_cloudprovider.go +++ b/pkg/hostsensormanager/sensor_cloudprovider.go @@ -22,6 +22,11 @@ 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{ diff --git a/pkg/hostsensormanager/sensor_cni.go b/pkg/hostsensormanager/sensor_cni.go index 571b42ddff..bf86a9d0e2 100644 --- a/pkg/hostsensormanager/sensor_cni.go +++ b/pkg/hostsensormanager/sensor_cni.go @@ -23,6 +23,11 @@ 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() diff --git a/pkg/hostsensormanager/sensor_controlplane.go b/pkg/hostsensormanager/sensor_controlplane.go index cbae078642..880fa2150c 100644 --- a/pkg/hostsensormanager/sensor_controlplane.go +++ b/pkg/hostsensormanager/sensor_controlplane.go @@ -37,6 +37,11 @@ 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() diff --git a/pkg/hostsensormanager/sensor_kernelvars.go b/pkg/hostsensormanager/sensor_kernelvars.go index 443ab0231d..9d185763bc 100644 --- a/pkg/hostsensormanager/sensor_kernelvars.go +++ b/pkg/hostsensormanager/sensor_kernelvars.go @@ -32,6 +32,11 @@ 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) diff --git a/pkg/hostsensormanager/sensor_kernelversion.go b/pkg/hostsensormanager/sensor_kernelversion.go index 30be3f1a2c..da0d8e2ab2 100644 --- a/pkg/hostsensormanager/sensor_kernelversion.go +++ b/pkg/hostsensormanager/sensor_kernelversion.go @@ -26,6 +26,11 @@ 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)) diff --git a/pkg/hostsensormanager/sensor_kubelet.go b/pkg/hostsensormanager/sensor_kubelet.go index fead793395..d956b198fb 100644 --- a/pkg/hostsensormanager/sensor_kubelet.go +++ b/pkg/hostsensormanager/sensor_kubelet.go @@ -41,6 +41,11 @@ 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() diff --git a/pkg/hostsensormanager/sensor_kubeproxy.go b/pkg/hostsensormanager/sensor_kubeproxy.go index 106f64cd71..449bcc6b2b 100644 --- a/pkg/hostsensormanager/sensor_kubeproxy.go +++ b/pkg/hostsensormanager/sensor_kubeproxy.go @@ -28,6 +28,11 @@ 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() diff --git a/pkg/hostsensormanager/sensor_network.go b/pkg/hostsensormanager/sensor_network.go index a212a2a1eb..6f9a8d7346 100644 --- a/pkg/hostsensormanager/sensor_network.go +++ b/pkg/hostsensormanager/sensor_network.go @@ -36,6 +36,11 @@ 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{ diff --git a/pkg/hostsensormanager/sensor_osrelease.go b/pkg/hostsensormanager/sensor_osrelease.go index df25f6bf74..5bc2821e58 100644 --- a/pkg/hostsensormanager/sensor_osrelease.go +++ b/pkg/hostsensormanager/sensor_osrelease.go @@ -32,6 +32,11 @@ 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() diff --git a/pkg/hostsensormanager/sensor_security.go b/pkg/hostsensormanager/sensor_security.go index 12e65d5c12..ac0140301d 100644 --- a/pkg/hostsensormanager/sensor_security.go +++ b/pkg/hostsensormanager/sensor_security.go @@ -26,6 +26,11 @@ 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{ diff --git a/pkg/hostsensormanager/types.go b/pkg/hostsensormanager/types.go index 40a8f1a09b..c9a08e5c81 100644 --- a/pkg/hostsensormanager/types.go +++ b/pkg/hostsensormanager/types.go @@ -27,6 +27,8 @@ type Sensor interface { 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) From 8065ecb0a76d7ae534cebacdb7850c4dbf925349 Mon Sep 17 00:00:00 2001 From: Bezalel Brandwine Date: Mon, 5 Jan 2026 10:56:23 +0200 Subject: [PATCH 4/6] CRDs only in helm-charts repo Signed-off-by: Bezalel Brandwine --- deploy/crds/osreleasefile-crd.yaml | 62 ------------------------------ 1 file changed, 62 deletions(-) delete mode 100644 deploy/crds/osreleasefile-crd.yaml diff --git a/deploy/crds/osreleasefile-crd.yaml b/deploy/crds/osreleasefile-crd.yaml deleted file mode 100644 index fbc2737f84..0000000000 --- a/deploy/crds/osreleasefile-crd.yaml +++ /dev/null @@ -1,62 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: osreleasefiles.hostdata.kubescape.cloud - annotations: - controller-gen.kubebuilder.io/version: v0.11.1 -spec: - group: hostdata.kubescape.cloud - names: - kind: OsReleaseFile - listKind: OsReleaseFileList - plural: osreleasefiles - singular: osreleasefile - scope: Cluster - versions: - - name: v1beta1 - served: true - storage: true - schema: - openAPIV3Schema: - description: OsReleaseFile contains the OS release information from a node - type: object - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object.' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents.' - type: string - metadata: - type: object - spec: - description: OsReleaseFileSpec contains the actual OS release file content - type: object - properties: - content: - description: Content is the raw content of the OS release file - type: string - nodeName: - description: NodeName is the name of the node this data came from - type: string - status: - description: OsReleaseFileStatus contains status information about the sensing - type: object - properties: - lastSensed: - description: LastSensed is the timestamp when this data was last collected - type: string - format: date-time - error: - description: Error contains any error message from the last sensing attempt - type: string - additionalPrinterColumns: - - name: Node - type: string - jsonPath: .spec.nodeName - - name: Last Sensed - type: string - jsonPath: .status.lastSensed - - name: Age - type: date - jsonPath: .metadata.creationTimestamp From df0557d0647b984dd1cb2836232c5dd78eb7af24 Mon Sep 17 00:00:00 2001 From: Bezalel Brandwine Date: Mon, 5 Jan 2026 11:43:57 +0200 Subject: [PATCH 5/6] fix TestLoadConfig Signed-off-by: Bezalel Brandwine --- configuration/config.json | 4 +++- pkg/config/config_test.go | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) 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/pkg/config/config_test.go b/pkg/config/config_test.go index c823d87b65..a28c07de7c 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, From 9d1811b3707c63565b445d6dc4ec0fecd4d354f6 Mon Sep 17 00:00:00 2001 From: Bezalel Brandwine Date: Sun, 18 Jan 2026 13:41:13 +0200 Subject: [PATCH 6/6] Address dear matthyx comments Signed-off-by: Bezalel Brandwine --- cmd/main.go | 21 +++++---- pkg/hostsensormanager/crd_client.go | 61 +++++++++++++-------------- pkg/hostsensormanager/manager.go | 51 ++++++++++------------ pkg/hostsensormanager/sensor_utils.go | 11 ++++- 4 files changed, 74 insertions(+), 70 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index fd05b475a3..8e04b539e5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -183,14 +183,19 @@ func main() { dWatcher.AddAdaptor(k8sObjectCache) // Create the host sensor manager - 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)) + 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 diff --git a/pkg/hostsensormanager/crd_client.go b/pkg/hostsensormanager/crd_client.go index 22a3f1af5d..e65e223f12 100644 --- a/pkg/hostsensormanager/crd_client.go +++ b/pkg/hostsensormanager/crd_client.go @@ -8,6 +8,7 @@ import ( "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" @@ -63,48 +64,44 @@ func (c *CRDClient) CreateOrUpdateHostData(ctx context.Context, resource string, }, } - // Try to get existing resource - _, err := c.dynamicClient.Resource(gvr).Get(ctx, c.nodeName, metav1.GetOptions{}) - if err != nil { - // Resource doesn't exist, create it - logger.L().Debug("creating new host data CRD", - helpers.String("kind", kind), - helpers.String("nodeName", c.nodeName)) - _, err = c.dynamicClient.Resource(gvr).Create(ctx, unstructuredObj, metav1.CreateOptions{}) - if err != nil { - return fmt.Errorf("failed to create %s CRD: %w", kind, err) - } + // 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 } - // Resource exists, update it using patch - logger.L().Debug("updating existing host data CRD", - helpers.String("kind", kind), - helpers.String("nodeName", c.nodeName)) + // 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)) - // Create patch data - patchData, err := json.Marshal(map[string]interface{}{ - "spec": spec, - "status": Status{ - LastSensed: metav1.Now(), - }, - }) - if err != nil { - return fmt.Errorf("failed to marshal patch data: %w", err) - } + // 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) + } - _, err = c.dynamicClient.Resource(gvr).Patch(ctx, c.nodeName, types.MergePatchType, patchData, metav1.PatchOptions{}) - if err != nil { - return fmt.Errorf("failed to patch %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 } - 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 diff --git a/pkg/hostsensormanager/manager.go b/pkg/hostsensormanager/manager.go index 6dd275b208..4c9d3f5875 100644 --- a/pkg/hostsensormanager/manager.go +++ b/pkg/hostsensormanager/manager.go @@ -17,15 +17,13 @@ type manager struct { sensors []Sensor stopCh chan struct{} wg sync.WaitGroup - mu sync.Mutex - running bool + startOnce sync.Once } // NewHostSensorManager creates a new host sensor manager func NewHostSensorManager(config Config) (HostSensorManager, error) { if !config.Enabled { - logger.L().Info("host sensor manager is disabled") - return &noopManager{}, nil + return NewNoopHostSensorManager(), nil } if config.NodeName == "" { @@ -65,40 +63,32 @@ func NewHostSensorManager(config Config) (HostSensorManager, error) { // Start begins the sensing loop func (m *manager) Start(ctx context.Context) error { - m.mu.Lock() - if m.running { - m.mu.Unlock() - return fmt.Errorf("manager is already running") - } - m.running = true - m.mu.Unlock() - - logger.L().Info("starting host sensor manager", - helpers.String("nodeName", m.config.NodeName), - helpers.String("interval", m.config.Interval.String())) + 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) + // Run initial sensing immediately + m.runSensing(ctx) - // Start periodic sensing - m.wg.Add(1) - go m.sensingLoop(ctx) + // Start periodic sensing + m.wg.Add(1) + go m.sensingLoop(ctx) + }) return nil } // Stop gracefully stops the manager func (m *manager) Stop() error { - m.mu.Lock() - if !m.running { - m.mu.Unlock() + logger.L().Info("stopping host sensor manager") + select { + case <-m.stopCh: + // Already closed return nil + default: + close(m.stopCh) } - m.running = false - m.mu.Unlock() - - logger.L().Info("stopping host sensor manager") - close(m.stopCh) m.wg.Wait() logger.L().Info("host sensor manager stopped") return nil @@ -169,6 +159,11 @@ func (m *manager) runSensor(ctx context.Context, sensor Sensor) error { // 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 } diff --git a/pkg/hostsensormanager/sensor_utils.go b/pkg/hostsensormanager/sensor_utils.go index 37798863ca..b6a4749b02 100644 --- a/pkg/hostsensormanager/sensor_utils.go +++ b/pkg/hostsensormanager/sensor_utils.go @@ -15,10 +15,17 @@ import ( ) const ( - hostFSPrefix = "/host_fs" // Mount point for host filesystem - procDirName = "/proc" + 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