Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ The **Kubescape Operator** is the central orchestration engine for [Kubescape](h
- [Features](#features)
- [Continuous Scanning](#continuous-scanning)
- [Admission Controller](#admission-controller)
- [SBOM Export to Dependency-Track](#sbom-export-to-dependency-track)
- [Development](#development)
- [Building from Source](#building-from-source)
- [VS Code Configuration](#vs-code-configuration)
Expand Down Expand Up @@ -239,7 +240,8 @@ The Operator reads configuration from `/etc/config/`. When running locally, set
"nodeSbomGeneration": "disable",
"seccomp": "disable",
"otel": "enable",
"admissionController": "disable"
"admissionController": "disable",
"sbomExport": "disable"
},
"components": {
"operator": { "enabled": true },
Expand Down Expand Up @@ -321,6 +323,12 @@ The Operator reads configuration from `/etc/config/`. When running locally, set
| `excludeControls` | `[]` | Control IDs or legacy CIS section numbers omitted from operator posture scans |
| `excludeNamespacesRegex` | `""` | Comma-separated string or array of RE2 regex patterns for namespaces to exclude |
| `namespaceFilterConfigMapName` | `""` | Optional ConfigMap name in the operator namespace for live namespace filtering |
| `sbomExport.resyncInterval` | `1h` | How often the [SBOM export](docs/features/sbom-export.md) compares every running image with Dependency-Track |
| `sbomExport.maxSBOMSize` | `10485760` | Largest SBOM, in bytes, the SBOM export converts; the operator needs about nine times this in memory, see [SBOM size](docs/features/sbom-export.md#sbom-size) |
| `sbomExport.dependencyTrack.url` | `""` | Dependency-Track API server the SBOM export sends to |
| `sbomExport.dependencyTrack.apiKeyFile` | `/etc/sbom-export/api-key` | File holding the Dependency-Track API key, read on every request |
| `sbomExport.dependencyTrack.rootProject` | `{{ .Cluster }}` | Root project, where created parents go; see [changing the layout](docs/features/sbom-export.md#changing-the-layout) |
| `sbomExport.dependencyTrack.project` | `{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}` : `{{ .Tag }}`, parent `{{ .Cluster }}/{{ .Namespace }}` | Where each image goes; namespaces can override it with `kubescape.io/dtrack-*` annotations |

### Exclude controls from posture scans

Expand Down Expand Up @@ -807,6 +815,22 @@ helm upgrade kubescape kubescape/kubescape-operator \

When enabled, the Operator runs an HTTPS webhook server on port `8443` that integrates with Kubernetes admission control.

### SBOM Export to Dependency-Track

The Operator sends the SBOM of every running image, as node-agent built it, to [Dependency-Track](https://dependencytrack.org): one project per image and namespace, kept current as workloads change. It needs `nodeSbomGeneration`, but neither kubevuln nor registry access. Where projects go is configurable, and namespaces can send their images under projects that already exist with `kubescape.io/dtrack-*` annotations.

**Enable via Helm**, with the API key in a Secret you create in the Kubescape namespace:
```bash
helm upgrade kubescape kubescape/kubescape-operator \
-n kubescape \
--set capabilities.nodeSbomGeneration=enable \
--set capabilities.sbomExport=enable \
--set sbomExport.dependencyTrack.url=https://dtrack.example.com \
--set sbomExport.dependencyTrack.apiKeySecret.name=dependency-track
```

See [SBOM export to Dependency-Track](docs/features/sbom-export.md) for the project layout and its annotations, the size limit, the settings and the permissions the API key needs.

---

## Development
Expand Down
72 changes: 72 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Capabilities struct {
VulnerabilityScan string `json:"vulnerabilityScan"`
AdmissionController string `json:"admissionController"`
RiskAcceptance string `json:"riskAcceptance"`
SBOMExport string `json:"sbomExport"`
}

type Components struct {
Expand Down Expand Up @@ -156,6 +157,51 @@ type Config struct {
RulesUpdateConfig rulesupdate.RulesUpdaterConfig `mapstructure:"rulesUpdateConfig"`
SkipProfilesWithoutInstances bool `mapstructure:"skipProfilesWithoutInstances"`
NodeAgentAutoscaler NodeAgentAutoscalerConfig `mapstructure:"nodeAgentAutoscaler"`
SBOMExport SBOMExportConfig `mapstructure:"sbomExport"`
}

// SBOMExportConfig configures the export of node SBOMs to Dependency-Track,
// turned on by the sbomExport capability.
type SBOMExportConfig struct {
// ResyncInterval is how often the exporter compares what runs in the
// cluster with what Dependency-Track holds.
ResyncInterval time.Duration `mapstructure:"resyncInterval"`
// MaxSBOMSize is the largest SBOM, in bytes, the exporter converts; larger
// ones are skipped, which bounds the operator's memory.
MaxSBOMSize int64 `mapstructure:"maxSBOMSize"`
DependencyTrack DependencyTrackConfig `mapstructure:"dependencyTrack"`
}

// DependencyTrackConfig is where the exporter sends the SBOMs, and where in
// Dependency-Track's project tree they go. Names and versions are Go
// templates over the cluster, the namespace, its labels, and the image's
// repository and tag; Namespace annotations can override them.
type DependencyTrackConfig struct {
// URL is the base URL of the Dependency-Track API server.
URL string `mapstructure:"url"`
// APIKeyFile is the file the API key is read from, on every request, so a
// rotated Secret needs no restart.
APIKeyFile string `mapstructure:"apiKeyFile"`
// RootProject is where the exporter creates the parents it creates.
RootProject DTCollectionConfig `mapstructure:"rootProject"`
// Project is the project of one image in one namespace.
Project DTProjectConfig `mapstructure:"project"`
}

// DTCollectionConfig is a collection project: the root, or an image's parent.
type DTCollectionConfig struct {
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
// Create lets the exporter create the project when it is missing. Without
// it the project must exist, and the exporter never changes it.
Create bool `mapstructure:"create"`
}

// DTProjectConfig is the project an image is uploaded to, and its parent.
type DTProjectConfig struct {
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
Parent DTCollectionConfig `mapstructure:"parent"`
}

// IConfig is an interface for all config types used in the operator
Expand All @@ -170,6 +216,8 @@ type IConfig interface {
ContinuousScanEnabled() bool
NodeSbomGenerationEnabled() bool
RiskAcceptanceEnabled() bool
SBOMExportEnabled() bool
SBOMExportConfig() SBOMExportConfig
CleanUpRoutineInterval() time.Duration
MatchingRulesFilename() string
TriggerSecurityFramework() bool
Expand Down Expand Up @@ -250,6 +298,16 @@ func (c *OperatorConfig) NodeSbomGenerationEnabled() bool {
return c.components.Capabilities.NodeSbomGeneration == "enable"
}

// SBOMExportEnabled reports whether the sbomExport capability is on.
func (c *OperatorConfig) SBOMExportEnabled() bool {
return c.components.Capabilities.SBOMExport == "enable"
}

// SBOMExportConfig returns where and how often to export SBOMs.
func (c *OperatorConfig) SBOMExportConfig() SBOMExportConfig {
return c.serviceConfig.SBOMExport
}

// RiskAcceptanceEnabled reports whether the riskAcceptance capability is on. The
// SecurityException watcher must gate on this because the operator's RBAC for
// securityexceptions/clustersecurityexceptions is only granted by the Helm chart
Expand Down Expand Up @@ -391,6 +449,7 @@ func (c *OperatorConfig) NodeAgentAutoscalerConfig() NodeAgentAutoscalerConfig {
return c.serviceConfig.NodeAgentAutoscaler
}

// LoadConfig reads config.json from path over the defaults set here.
func LoadConfig(path string) (Config, error) {
viper.AddConfigPath(path)
viper.SetConfigName("config")
Expand Down Expand Up @@ -432,6 +491,19 @@ func LoadConfig(path string) (Config, error) {
viper.SetDefault("nodeAgentAutoscaler.seLinuxType", "spc_t")
viper.SetDefault("nodeAgentAutoscaler.bottlerocketAutoDetect", true)

// SBOM export defaults: the project tree is <cluster>/<namespace>/<repository>:<tag>
viper.SetDefault("sbomExport.resyncInterval", time.Hour)
viper.SetDefault("sbomExport.maxSBOMSize", 10<<20)
viper.SetDefault("sbomExport.dependencyTrack.apiKeyFile", "/etc/sbom-export/api-key")
viper.SetDefault("sbomExport.dependencyTrack.rootProject.name", "{{ .Cluster }}")
viper.SetDefault("sbomExport.dependencyTrack.rootProject.version", "current")
viper.SetDefault("sbomExport.dependencyTrack.rootProject.create", true)
viper.SetDefault("sbomExport.dependencyTrack.project.name", "{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}")
viper.SetDefault("sbomExport.dependencyTrack.project.version", "{{ .Tag }}")
viper.SetDefault("sbomExport.dependencyTrack.project.parent.name", "{{ .Cluster }}/{{ .Namespace }}")
viper.SetDefault("sbomExport.dependencyTrack.project.parent.version", "current")
viper.SetDefault("sbomExport.dependencyTrack.project.parent.create", true)

viper.AutomaticEnv()

err := viper.ReadInConfig()
Expand Down
91 changes: 91 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func TestLoadCapabilities(t *testing.T) {
}
}

// TestLoadConfig loads ../configuration/config.json and compares the result.
func TestLoadConfig(t *testing.T) {
type args struct {
path string
Expand Down Expand Up @@ -179,6 +180,19 @@ func TestLoadConfig(t *testing.T) {
SELinuxType: "spc_t",
BottlerocketAutoDetect: true,
},
SBOMExport: SBOMExportConfig{
ResyncInterval: time.Hour,
MaxSBOMSize: 10 << 20,
DependencyTrack: DependencyTrackConfig{
APIKeyFile: "/etc/sbom-export/api-key",
RootProject: DTCollectionConfig{Name: "{{ .Cluster }}", Version: "current", Create: true},
Project: DTProjectConfig{
Name: "{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}",
Version: "{{ .Tag }}",
Parent: DTCollectionConfig{Name: "{{ .Cluster }}/{{ .Namespace }}", Version: "current", Create: true},
},
},
},
},
},
}
Expand Down Expand Up @@ -619,3 +633,80 @@ func TestNewOperatorConfig_RegexValidation(t *testing.T) {
assert.False(t, opCfg.SkipNamespace("default"))
})
}

// TestSBOMExportEnabled checks that only "enable" turns the export on.
func TestSBOMExportEnabled(t *testing.T) {
tests := []struct {
value string
want bool
}{
{"enable", true},
{"disable", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.value, func(t *testing.T) {
cfg, err := NewOperatorConfig(
CapabilitiesConfig{Capabilities: Capabilities{SBOMExport: tt.value}},
armometadata.ClusterConfig{},
&utils.Credentials{},
Config{},
)
require.NoError(t, err)
assert.Equal(t, tt.want, cfg.SBOMExportEnabled())
})
}
}

func TestLoadConfigSBOMExportDefaults(t *testing.T) {
viper.Reset()
defer viper.Reset()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{}`), 0600))
cfg, err := LoadConfig(dir)
require.NoError(t, err)
assert.Equal(t, time.Hour, cfg.SBOMExport.ResyncInterval)
assert.Equal(t, int64(10<<20), cfg.SBOMExport.MaxSBOMSize)
assert.Equal(t, "/etc/sbom-export/api-key", cfg.SBOMExport.DependencyTrack.APIKeyFile)
assert.Empty(t, cfg.SBOMExport.DependencyTrack.URL)
assert.Equal(t, DTCollectionConfig{Name: "{{ .Cluster }}", Version: "current", Create: true}, cfg.SBOMExport.DependencyTrack.RootProject)
assert.Equal(t, DTProjectConfig{
Name: "{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}",
Version: "{{ .Tag }}",
Parent: DTCollectionConfig{Name: "{{ .Cluster }}/{{ .Namespace }}", Version: "current", Create: true},
}, cfg.SBOMExport.DependencyTrack.Project)
}

func TestLoadConfigSBOMExport(t *testing.T) {
viper.Reset()
defer viper.Reset()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{
"sbomExport": {
"resyncInterval": "30m",
"maxSBOMSize": 5242880,
"dependencyTrack": {
"url": "https://dtrack.example.com",
"apiKeyFile": "/keys/dtrack",
"rootProject": {"name": "Finance", "version": "current", "create": false},
"project": {"name": "{{ .Repository }}", "parent": {"name": "Payments Team", "create": false}}
}
}
}`), 0600))
cfg, err := LoadConfig(dir)
require.NoError(t, err)
assert.Equal(t, SBOMExportConfig{
ResyncInterval: 30 * time.Minute,
MaxSBOMSize: 5 << 20,
DependencyTrack: DependencyTrackConfig{
URL: "https://dtrack.example.com",
APIKeyFile: "/keys/dtrack",
RootProject: DTCollectionConfig{Name: "Finance", Version: "current", Create: false},
Project: DTProjectConfig{
Name: "{{ .Repository }}",
Version: "{{ .Tag }}",
Parent: DTCollectionConfig{Name: "Payments Team", Version: "current", Create: false},
},
},
}, cfg.SBOMExport, "unset fields keep their defaults")
}
Loading
Loading