diff --git a/.golangci.yml b/.golangci.yml index 825af29..3e7ff0d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,6 +18,7 @@ linters: - exhaustruct - paralleltest - testpackage + - noinlineerr issues: max-issues-per-linter: 0 max-same-issues: 0 diff --git a/README.md b/README.md index 751c98e..8b72e86 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,84 @@ make build Run the server: ```bash +# With configuration file +./stackrox-mcp --config=examples/config-read-only.yaml + +# Or using environment variables only +export STACKROX_MCP__CENTRAL__URL=central.stackrox:8443 +export STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED=true ./stackrox-mcp ``` +## Configuration + +The StackRox MCP server supports configuration through both YAML files and environment variables. Environment variables take precedence over YAML configuration. + +### Configuration File + +Specify a configuration file using the `--config` flag: + +```bash +./stackrox-mcp --config=/path/to/config.yaml +``` + +See [examples/config-read-only.yaml](examples/config-read-only.yaml) for a complete configuration example. + +### Environment Variables + +All configuration options can be set via environment variables using the naming convention: + +``` +STACKROX_MCP__SECTION__KEY +``` + +Note the double underscore (`__`) separator between sections and keys. + +#### Examples + +```bash +export STACKROX_MCP__CENTRAL__URL=central.stackrox:8443 +export STACKROX_MCP__GLOBAL__READ_ONLY_TOOLS=true +export STACKROX_MCP__TOOLS__CONFIG_MANAGER__ENABLED=true +``` + +### Configuration Options + +#### Central Configuration + +Configuration for connecting to StackRox Central. + +| Option | Environment Variable | Type | Required | Default | Description | +|--------|---------------------|------|----------|---------|-------------| +| `central.url` | `STACKROX_MCP__CENTRAL__URL` | string | Yes | central.stackrox:8443 | URL of StackRox Central instance | +| `central.insecure` | `STACKROX_MCP__CENTRAL__INSECURE` | bool | No | `false` | Skip TLS certificate verification | +| `central.force_http1` | `STACKROX_MCP__CENTRAL__FORCE_HTTP1` | bool | No | `false` | Force HTTP/1.1 instead of HTTP/2 | + +#### Global Configuration + +Global MCP server settings. + +| Option | Environment Variable | Type | Required | Default | Description | +|--------|---------------------|------|----------|---------|-------------| +| `global.read_only_tools` | `STACKROX_MCP__GLOBAL__READ_ONLY_TOOLS` | bool | No | `true` | Only allow read-only tools | + +#### Tools Configuration + +Enable or disable individual MCP tools. At least one tool has to be enabled. + +| Option | Environment Variable | Type | Required | Default | Description | +|--------|---------------------|------|----------|---------|-------------| +| `tools.vulnerability.enabled` | `STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED` | bool | No | `false` | Enable vulnerability management tools | +| `tools.config_manager.enabled` | `STACKROX_MCP__TOOLS__CONFIG_MANAGER__ENABLED` | bool | No | `false` | Enable configuration management tools | + +### Configuration Precedence + +Configuration values are loaded in the following order (later sources override earlier ones): + +1. Default values +2. YAML configuration file (if provided via `--config`) +3. Environment variables (highest precedence) + ## Development For detailed development guidelines, testing standards, and contribution workflows, see [CONTRIBUTING.md](.github/CONTRIBUTING.md). diff --git a/cmd/stackrox-mcp/main.go b/cmd/stackrox-mcp/main.go index 91e1870..2c81c41 100644 --- a/cmd/stackrox-mcp/main.go +++ b/cmd/stackrox-mcp/main.go @@ -2,13 +2,28 @@ package main import ( + "flag" "log/slog" + "os" + "github.com/stackrox/stackrox-mcp/internal/config" "github.com/stackrox/stackrox-mcp/internal/logging" ) func main() { logging.SetupLogging() + configPath := flag.String("config", "", "Path to configuration file (optional)") + + flag.Parse() + + cfg, err := config.LoadConfig(*configPath) + if err != nil { + slog.Error("Failed to load configuration", "error", err) + os.Exit(1) + } + + slog.Info("Configuration loaded successfully", "config", cfg) + slog.Info("Starting Stackrox MCP server") } diff --git a/examples/config-read-only.yaml b/examples/config-read-only.yaml new file mode 100644 index 0000000..2c81cd9 --- /dev/null +++ b/examples/config-read-only.yaml @@ -0,0 +1,50 @@ +# StackRox MCP Server Configuration +# +# This is an example configuration file for the StackRox MCP server. +# Copy this file and modify it according to your environment. +# +# Environment Variable Mapping: +# All configuration options can be overridden using environment variables. +# Environment variables take precedence over YAML configuration. +# +# Naming convention: STACKROX_MCP__SECTION__KEY +# Example: +# central: +# url: central.stackrox:8443 +# +# Can be overridden with: +# STACKROX_MCP__CENTRAL__URL=central.stackrox:8443 + +# Central connection configuration +central: + # Central URL (required, default: central.stackrox:8443) + # The URL of your StackRox Central instance + url: central.stackrox:8443 + + # Allow insecure TLS connection (optional, default: false) + # Set to true to skip TLS certificate verification + insecure: false + + # Force HTTP1 (optional, default: false) + # Force HTTP/1.1 instead of HTTP/2 + force_http1: false + +# Global MCP server configuration +global: + # Allow only read-only MCP tools (optional, default: true) + # When true, only tools that perform read operations are available + # When false, both read and write tools may be available (if implemented) + read_only_tools: true + +# Configuration of MCP tools +# Each tool has an enable/disable flag. At least one tool has to be enabled. +tools: + # Vulnerability management tools + vulnerability: + # Enable vulnerability management tools (optional, default: false) + enabled: true + + # Configuration management tools + config_manager: + # Enable configuration management tools (optional, default: false) + enabled: true diff --git a/go.mod b/go.mod index b470f12..0aa36fb 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,26 @@ module github.com/stackrox/stackrox-mcp go 1.24 -require github.com/stretchr/testify v1.11.1 +require ( + github.com/pkg/errors v0.9.1 + github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.11.1 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c4c1710..de4a809 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,49 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..175d63d --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,110 @@ +// Package config provides configuration handling for StackRox MCP server. +package config + +import ( + "strings" + + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +// Config represents the complete application configuration. +type Config struct { + Central CentralConfig `mapstructure:"central"` + Global GlobalConfig `mapstructure:"global"` + Tools ToolsConfig `mapstructure:"tools"` +} + +// CentralConfig contains StackRox Central connection configuration. +type CentralConfig struct { + URL string `mapstructure:"url"` + Insecure bool `mapstructure:"insecure"` + ForceHTTP1 bool `mapstructure:"force_http1"` +} + +// GlobalConfig contains global MCP server configuration. +type GlobalConfig struct { + ReadOnlyTools bool `mapstructure:"read_only_tools"` +} + +// ToolsConfig contains configuration for individual MCP tools. +type ToolsConfig struct { + Vulnerability ToolsetVulnerabilityConfig `mapstructure:"vulnerability"` + ConfigManager ToolConfigManagerConfig `mapstructure:"config_manager"` +} + +// ToolsetVulnerabilityConfig contains configuration for vulnerability management tools. +type ToolsetVulnerabilityConfig struct { + Enabled bool `mapstructure:"enabled"` +} + +// ToolConfigManagerConfig contains configuration for config management tools. +type ToolConfigManagerConfig struct { + Enabled bool `mapstructure:"enabled"` +} + +// LoadConfig loads configuration from YAML file and environment variables. +// Environment variables take precedence over YAML configuration. +// Env var naming convention: STACKROX_MCP__SECTION__KEY (double underscore as separator). +// configPath: optional path to YAML configuration file (can be empty). +func LoadConfig(configPath string) (*Config, error) { + viperInstance := viper.New() + + setDefaults(viperInstance) + + // Set up environment variable support. + // Note: SetEnvPrefix adds a single underscore, so "STACKROX_MCP_" becomes the prefix. + // We want double underscores between sections, so we use "__" in the replacer. + viperInstance.SetEnvPrefix("STACKROX_MCP_") + viperInstance.SetEnvKeyReplacer(strings.NewReplacer(".", "__")) + viperInstance.AutomaticEnv() + + if configPath != "" { + viperInstance.SetConfigFile(configPath) + + if err := viperInstance.ReadInConfig(); err != nil { + return nil, errors.Wrap(err, "failed to read config file") + } + } + + var cfg Config + if err := viperInstance.Unmarshal(&cfg); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal config") + } + + if err := cfg.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid configuration") + } + + return &cfg, nil +} + +// setDefaults sets default values for configuration. +func setDefaults(viper *viper.Viper) { + viper.SetDefault("central.url", "central.stackrox:8443") + viper.SetDefault("central.insecure", false) + viper.SetDefault("central.force_http1", false) + + viper.SetDefault("global.read_only_tools", true) + + viper.SetDefault("tools.vulnerability.enabled", false) + viper.SetDefault("tools.config_manager.enabled", false) +} + +var ( + errURLRequired = errors.New("central.url is required") + errAtLeastOneTool = errors.New("at least one tool has to be enabled") +) + +// Validate validates the configuration. +func (c *Config) Validate() error { + if c.Central.URL == "" { + return errURLRequired + } + + if !c.Tools.Vulnerability.Enabled && !c.Tools.ConfigManager.Enabled { + return errAtLeastOneTool + } + + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..6b07419 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,194 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadConfig_FromYAML(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.yaml") + + yamlContent := ` +central: + url: central.example.com:8443 + insecure: true + force_http1: true +global: + read_only_tools: false +tools: + vulnerability: + enabled: true + config_manager: + enabled: False +` + err := os.WriteFile(configPath, []byte(yamlContent), 0600) + require.NoError(t, err) + + defer func() { assert.NoError(t, os.Remove(configPath)) }() + + cfg, err := LoadConfig(configPath) + require.NoError(t, err) + require.NotNil(t, cfg) + + assert.Equal(t, "central.example.com:8443", cfg.Central.URL) + assert.True(t, cfg.Central.Insecure) + assert.True(t, cfg.Central.ForceHTTP1) + assert.False(t, cfg.Global.ReadOnlyTools) + assert.True(t, cfg.Tools.Vulnerability.Enabled) + assert.False(t, cfg.Tools.ConfigManager.Enabled) +} + +func TestLoadConfig_EnvVarOverride(t *testing.T) { + yamlContent := ` +central: + url: central.example.com:8443 +tools: + vulnerability: + enabled: false +` + + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.yaml") + err := os.WriteFile(configPath, []byte(yamlContent), 0600) + require.NoError(t, err) + + defer func() { assert.NoError(t, os.Remove(configPath)) }() + + t.Setenv("STACKROX_MCP__CENTRAL__URL", "override.example.com:443") + t.Setenv("STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED", "true") + + cfg, err := LoadConfig(configPath) + require.NoError(t, err) + require.NotNil(t, cfg) + + assert.Equal(t, "override.example.com:443", cfg.Central.URL) + assert.True(t, cfg.Tools.Vulnerability.Enabled) +} + +func TestLoadConfig_EnvVarOnly(t *testing.T) { + t.Setenv("STACKROX_MCP__CENTRAL__URL", "env.example.com:8443") + t.Setenv("STACKROX_MCP__CENTRAL__INSECURE", "true") + t.Setenv("STACKROX_MCP__CENTRAL__FORCE_HTTP1", "true") + t.Setenv("STACKROX_MCP__GLOBAL__READ_ONLY_TOOLS", "false") + t.Setenv("STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED", "true") + t.Setenv("STACKROX_MCP__TOOLS__CONFIG_MANAGER__ENABLED", "true") + + cfg, err := LoadConfig("") + require.NoError(t, err) + require.NotNil(t, cfg) + + assert.Equal(t, "env.example.com:8443", cfg.Central.URL) + assert.True(t, cfg.Central.Insecure) + assert.True(t, cfg.Central.ForceHTTP1) + assert.False(t, cfg.Global.ReadOnlyTools) + assert.True(t, cfg.Tools.Vulnerability.Enabled) + assert.True(t, cfg.Tools.ConfigManager.Enabled) +} + +func TestLoadConfig_Defaults(t *testing.T) { + // Set only required field + t.Setenv("STACKROX_MCP__TOOLS__CONFIG_MANAGER__ENABLED", "true") + + cfg, err := LoadConfig("") + require.NoError(t, err) + require.NotNil(t, cfg) + + assert.Equal(t, "central.stackrox:8443", cfg.Central.URL) + assert.False(t, cfg.Central.Insecure) + assert.False(t, cfg.Central.ForceHTTP1) + assert.True(t, cfg.Global.ReadOnlyTools) + assert.False(t, cfg.Tools.Vulnerability.Enabled) + assert.True(t, cfg.Tools.ConfigManager.Enabled) + + // Check another tools default. + t.Setenv("STACKROX_MCP__TOOLS__CONFIG_MANAGER__ENABLED", "") + t.Setenv("STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED", "true") + + cfg, err = LoadConfig("") + require.NoError(t, err) + require.NotNil(t, cfg) + + assert.True(t, cfg.Tools.Vulnerability.Enabled) + assert.False(t, cfg.Tools.ConfigManager.Enabled) +} + +func TestLoadConfig_MissingFile(t *testing.T) { + _, err := LoadConfig("/non/existent/config.yaml") + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to read config file") +} + +func TestLoadConfig_InvalidYAML(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.yaml") + + invalidYAML := ` +central: + url: central.example.com:8443 + invalid yaml syntax here: [[[ +` + + err := os.WriteFile(configPath, []byte(invalidYAML), 0600) + require.NoError(t, err) + + _, err = LoadConfig(configPath) + assert.Error(t, err) +} + +func TestValidate_MissingURL(t *testing.T) { + cfg := &Config{ + Central: CentralConfig{ + URL: "", + }, + Tools: ToolsConfig{ + Vulnerability: ToolsetVulnerabilityConfig{ + Enabled: true, + }, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "central.url is required") +} + +func TestValidate_AtLeastOneTool(t *testing.T) { + cfg := &Config{ + Central: CentralConfig{ + URL: "central.example.com:8443", + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "at least one tool has to be enabled") +} + +func TestValidate_ValidConfig(t *testing.T) { + cfg := &Config{ + Central: CentralConfig{ + URL: "central.example.com:8443", + Insecure: false, + ForceHTTP1: false, + }, + Global: GlobalConfig{ + ReadOnlyTools: true, + }, + Tools: ToolsConfig{ + Vulnerability: ToolsetVulnerabilityConfig{ + Enabled: true, + }, + ConfigManager: ToolConfigManagerConfig{ + Enabled: false, + }, + }, + } + + err := cfg.Validate() + assert.NoError(t, err) +}