refactor(cli): Update the cloud config command (#9676)

This commit is contained in:
Owen Rumney
2025-10-20 16:57:08 +01:00
committed by GitHub
parent 2c43425e05
commit 559fe1fa2c
17 changed files with 931 additions and 119 deletions

View File

@@ -24,6 +24,5 @@ Control Trivy Cloud platform integration settings
### SEE ALSO
* [trivy](trivy.md) - Unified security scanner
* [trivy cloud edit-config](trivy_cloud_edit-config.md) - Edit Trivy Cloud configuration
* [trivy cloud show-config](trivy_cloud_show-config.md) - Show Trivy Cloud configuration
* [trivy cloud config](trivy_cloud_config.md) - Control Trivy Cloud configuration

View File

@@ -0,0 +1,32 @@
## trivy cloud config
Control Trivy Cloud configuration
### Options
```
-h, --help help for config
```
### Options inherited from parent commands
```
--cache-dir string cache directory (default "/path/to/cache")
-c, --config string config path (default "trivy.yaml")
-d, --debug debug mode
--generate-default-config write the default config to trivy-default.yaml
--insecure allow insecure server connections
-q, --quiet suppress progress bar and log output
--timeout duration timeout (default 5m0s)
-v, --version show version
```
### SEE ALSO
* [trivy cloud](trivy_cloud.md) - Control Trivy Cloud platform integration settings
* [trivy cloud config edit](trivy_cloud_config_edit.md) - Edit Trivy Cloud configuration
* [trivy cloud config get](trivy_cloud_config_get.md) - Get Trivy Cloud configuration
* [trivy cloud config list](trivy_cloud_config_list.md) - List Trivy Cloud configuration
* [trivy cloud config set](trivy_cloud_config_set.md) - Set Trivy Cloud configuration
* [trivy cloud config unset](trivy_cloud_config_unset.md) - Unset Trivy Cloud configuration

View File

@@ -1,19 +1,19 @@
## trivy cloud edit-config
## trivy cloud config edit
Edit Trivy Cloud configuration
### Synopsis
Edit the Trivy Cloud platform configuration in the default editor specified in the EDITOR environment variable
Edit Trivy Cloud platform configuration in the default editor specified in the EDITOR environment variable
```
trivy cloud edit-config [flags]
trivy cloud config edit [flags]
```
### Options
```
-h, --help help for edit-config
-h, --help help for edit
```
### Options inherited from parent commands
@@ -31,5 +31,5 @@ trivy cloud edit-config [flags]
### SEE ALSO
* [trivy cloud](trivy_cloud.md) - Control Trivy Cloud platform integration settings
* [trivy cloud config](trivy_cloud_config.md) - Control Trivy Cloud configuration

View File

@@ -0,0 +1,44 @@
## trivy cloud config get
Get Trivy Cloud configuration
### Synopsis
Get a Trivy Cloud platform configuration
Available config settings can be viewed by using the `trivy cloud config list` command
```
trivy cloud config get [setting] [flags]
```
### Examples
```
$ trivy cloud config get server.scanning.enabled
$ trivy cloud config get server.scanning.upload-results
```
### Options
```
-h, --help help for get
```
### Options inherited from parent commands
```
--cache-dir string cache directory (default "/path/to/cache")
-c, --config string config path (default "trivy.yaml")
-d, --debug debug mode
--generate-default-config write the default config to trivy-default.yaml
--insecure allow insecure server connections
-q, --quiet suppress progress bar and log output
--timeout duration timeout (default 5m0s)
-v, --version show version
```
### SEE ALSO
* [trivy cloud config](trivy_cloud_config.md) - Control Trivy Cloud configuration

View File

@@ -1,19 +1,19 @@
## trivy cloud show-config
## trivy cloud config list
Show Trivy Cloud configuration
List Trivy Cloud configuration
### Synopsis
Show Trivy Cloud platform configuration in human readable format
List Trivy Cloud platform configuration in human readable format
```
trivy cloud show-config [flags]
trivy cloud config list [flags]
```
### Options
```
-h, --help help for show-config
-h, --help help for list
```
### Options inherited from parent commands
@@ -31,5 +31,5 @@ trivy cloud show-config [flags]
### SEE ALSO
* [trivy cloud](trivy_cloud.md) - Control Trivy Cloud platform integration settings
* [trivy cloud config](trivy_cloud_config.md) - Control Trivy Cloud configuration

View File

@@ -0,0 +1,44 @@
## trivy cloud config set
Set Trivy Cloud configuration
### Synopsis
Set a Trivy Cloud platform setting
Available config settings can be viewed by using the `trivy cloud config list` command
```
trivy cloud config set [setting] [value] [flags]
```
### Examples
```
$ trivy cloud config set server.scanning.enabled true
$ trivy cloud config set server.scanning.upload-results false
```
### Options
```
-h, --help help for set
```
### Options inherited from parent commands
```
--cache-dir string cache directory (default "/path/to/cache")
-c, --config string config path (default "trivy.yaml")
-d, --debug debug mode
--generate-default-config write the default config to trivy-default.yaml
--insecure allow insecure server connections
-q, --quiet suppress progress bar and log output
--timeout duration timeout (default 5m0s)
-v, --version show version
```
### SEE ALSO
* [trivy cloud config](trivy_cloud_config.md) - Control Trivy Cloud configuration

View File

@@ -0,0 +1,44 @@
## trivy cloud config unset
Unset Trivy Cloud configuration
### Synopsis
Unset a Trivy Cloud platform configuration and return it to the default setting
Available config settings can be viewed by using the `trivy cloud config list` command
```
trivy cloud config unset [setting] [flags]
```
### Examples
```
$ trivy cloud config unset server.scanning.enabled
$ trivy cloud config unset server.scanning.upload-results
```
### Options
```
-h, --help help for unset
```
### Options inherited from parent commands
```
--cache-dir string cache directory (default "/path/to/cache")
-c, --config string config path (default "trivy.yaml")
-d, --debug debug mode
--generate-default-config write the default config to trivy-default.yaml
--insecure allow insecure server connections
-q, --quiet suppress progress bar and log output
--timeout duration timeout (default 5m0s)
-v, --version show version
```
### SEE ALSO
* [trivy cloud config](trivy_cloud_config.md) - Control Trivy Cloud configuration

View File

@@ -170,9 +170,13 @@ nav:
- Overview: docs/references/configuration/cli/trivy.md
- Clean: docs/references/configuration/cli/trivy_clean.md
- Cloud:
- Cloud: docs/references/configuration/cli/trivy_cloud.md
- Cloud Edit Config: docs/references/configuration/cli/trivy_cloud_edit-config.md
- Cloud Show Config: docs/references/configuration/cli/trivy_cloud_show-config.md
- Cloud: docs/references/configuration/cli/trivy_cloud.md
- Cloud Config: docs/references/configuration/cli/trivy_cloud_config.md
- Cloud Config Edit: docs/references/configuration/cli/trivy_cloud_config_edit.md
- Cloud Config List: docs/references/configuration/cli/trivy_cloud_config_list.md
- Cloud Config Set: docs/references/configuration/cli/trivy_cloud_config_set.md
- Cloud Config Unset: docs/references/configuration/cli/trivy_cloud_config_unset.md
- Cloud Config Get: docs/references/configuration/cli/trivy_cloud_config_get.md
- Config: docs/references/configuration/cli/trivy_config.md
- Convert: docs/references/configuration/cli/trivy_convert.md
- Filesystem: docs/references/configuration/cli/trivy_filesystem.md

View File

@@ -7,9 +7,10 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"reflect"
"strconv"
"strings"
"github.com/samber/lo"
"github.com/zalando/go-keyring"
@@ -28,21 +29,37 @@ const (
DefaultTrivyServerUrl = "https://scan.trivy.dev"
)
type Config struct {
ServerURL string `yaml:"server-url"`
ApiURL string `yaml:"api-url"`
ServerScanning bool `yaml:"server-scanning"`
UploadResults bool `yaml:"results-upload"`
type Api struct {
URL string `yaml:"url"`
}
type Scanning struct {
Enabled bool `yaml:"enabled"`
UploadResults bool `yaml:"upload-results"`
SecretConfig bool `yaml:"secret-config"`
MisconfigConfig bool `yaml:"misconfig-config"`
}
type Server struct {
URL string `yaml:"url"`
Scanning Scanning `yaml:"scanning"`
}
type Config struct {
Api Api `yaml:"api"`
Server Server `yaml:"server"`
IsLoggedIn bool `yaml:"-"`
Token string `yaml:"-"`
}
var defaultConfig = &Config{
ServerScanning: true,
UploadResults: true,
ServerURL: DefaultTrivyServerUrl,
ApiURL: DefaultApiUrl,
Api: Api{
URL: DefaultApiUrl,
},
Server: Server{
URL: DefaultTrivyServerUrl,
Scanning: Scanning{},
},
}
func getConfigPath() string {
@@ -51,10 +68,14 @@ func getConfigPath() string {
}
func (c *Config) Save() error {
if c.Token == "" && c.ServerURL == "" && c.ApiURL == "" {
if c.Token == "" && c.Server.URL == "" && c.Api.URL == "" {
return xerrors.New("no config to save, required fields are token, server url, and api url")
}
if err := c.initFirstLogin(); err != nil {
return err
}
if err := keyring.Set(ServiceName, TokenKey, c.Token); err != nil {
return err
}
@@ -90,6 +111,33 @@ func Clear() error {
return nil
}
// initFirstLogin initializes the default scanning settings to turn them on
// after this, the user can configure in the config using the config set/unset commands
func (c *Config) initFirstLogin() error {
if c.Token == "" {
// this isn't a login save, without a token it can't login
return nil
}
var firstLogin bool
_, err := keyring.Get(ServiceName, TokenKey)
if err != nil {
if !errors.Is(err, keyring.ErrNotFound) {
return err
}
firstLogin = true
}
if firstLogin {
// if first login, turn on all scanning options
c.Server.Scanning.Enabled = true
c.Server.Scanning.UploadResults = true
c.Server.Scanning.MisconfigConfig = true
c.Server.Scanning.SecretConfig = true
}
return nil
}
// Load loads the Trivy Cloud config from the config file and the keychain
// If the config file does not exist the default config is returned
func Load() (*Config, error) {
@@ -102,7 +150,8 @@ func Load() (*Config, error) {
return nil, err
}
logger.Debug("No cloud config file found")
return defaultConfig, nil
defaultCopy := *defaultConfig
return &defaultCopy, nil
}
if err := yaml.Unmarshal(yamlData, &config); err != nil {
return nil, err
@@ -114,7 +163,8 @@ func Load() (*Config, error) {
return nil, err
}
logger.Debug("No token found in keychain")
return defaultConfig, nil
config.Token = ""
return &config, nil
}
config.Token = token
@@ -128,18 +178,19 @@ func (c *Config) Verify(ctx context.Context) error {
return xerrors.New("no token provided for verification")
}
if c.ServerURL == "" {
if c.Server.URL == "" {
return xerrors.New("no server URL provided for verification")
}
logger := log.WithPrefix(log.PrefixCloud)
logger.Debug("Verifying Trivy Cloud token")
client := xhttp.Client()
url, err := url.JoinPath(c.ServerURL, "verify")
url, err := url.JoinPath(c.Server.URL, "verify")
if err != nil {
return xerrors.Errorf("failed to join server URL and verify path: %w", err)
}
logger.Debug("Verifying Trivy Cloud token against server", log.String("verification_url", url))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, http.NoBody)
if err != nil {
return xerrors.Errorf("failed to create verification request: %w", err)
@@ -159,29 +210,8 @@ func (c *Config) Verify(ctx context.Context) error {
}
// OpenConfigForEditing opens the Trivy Cloud config file for editing in the default editor specified in the EDITOR environment variable
func OpenConfigForEditing() error {
configPath := getConfigPath()
logger := log.WithPrefix(log.PrefixCloud)
if !fsutils.FileExists(configPath) {
logger.Debug("Trivy Cloud config file does not exist", log.String("config_path", configPath))
defaultConfig.Save()
configPath = getConfigPath()
}
editor := getEditCommand()
cmd := exec.Command(editor, configPath)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// ShowConfig shows the Trivy Cloud config in human readable format
func ShowConfig() error {
// ListConfig shows the Trivy Cloud config in human readable format
func ListConfig() error {
cloudConfig, err := Load()
if err != nil {
return xerrors.Errorf("failed to load Trivy Cloud config file: %w", err)
@@ -197,25 +227,72 @@ func ShowConfig() error {
fmt.Println()
fmt.Println("Trivy Cloud Configuration")
fmt.Println("-------------------------")
fmt.Printf("Logged In: %s\n", lo.Ternary(loggedIn, "Yes", "No"))
fmt.Printf("Trivy Server URL: %s\n", cloudConfig.ServerURL)
fmt.Printf("API URL: %s\n", cloudConfig.ApiURL)
fmt.Printf("Server Scanning: %s\n", lo.Ternary(cloudConfig.ServerScanning, "Enabled", "Disabled"))
fmt.Printf("Results Upload: %s\n", lo.Ternary(cloudConfig.UploadResults, "Enabled", "Disabled"))
fmt.Printf("Filepath: %s\n", getConfigPath())
fmt.Printf("Filepath: %s\n", getConfigPath())
fmt.Printf("Logged In: %s\n", lo.Ternary(loggedIn, "Yes", "No"))
fmt.Println()
fields := collectConfigFields(reflect.ValueOf(cloudConfig).Elem(), "")
maxKeyLen := 0
for _, field := range fields {
maxKeyLen = max(maxKeyLen, len(field.path))
}
for _, field := range fields {
fmt.Printf("%-*s %s\n", maxKeyLen, field.path, formatValue(field.value))
}
fmt.Println()
return nil
}
func getEditCommand() string {
editor := os.Getenv("EDITOR")
if editor != "" {
return editor
}
// fallback to notepad for windows or vi for macos/linux
if runtime.GOOS == "windows" {
return "notepad"
}
return "vi"
type configField struct {
path string
value reflect.Value
}
func collectConfigFields(v reflect.Value, prefix string) []configField {
var fields []configField
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldValue := v.Field(i)
yamlTag := field.Tag.Get("yaml")
if yamlTag == "-" || yamlTag == "" {
continue
}
tagName := strings.Split(yamlTag, ",")[0]
fullPath := tagName
if prefix != "" {
fullPath = prefix + "." + tagName
}
if fieldValue.Kind() == reflect.Struct {
fields = append(fields, collectConfigFields(fieldValue, fullPath)...)
} else {
fields = append(fields, configField{
path: fullPath,
value: fieldValue,
})
}
}
return fields
}
func formatValue(v reflect.Value) string {
switch v.Kind() {
case reflect.Bool:
return lo.Ternary(v.Bool(), "Enabled", "Disabled")
case reflect.String:
return v.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
case reflect.Float32, reflect.Float64:
return fmt.Sprintf("%f", v.Float())
default:
return fmt.Sprintf("%v", v.Interface())
}
}

44
pkg/cloud/config_edit.go Normal file
View File

@@ -0,0 +1,44 @@
package cloud
import (
"os"
"os/exec"
"runtime"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
)
// OpenConfigForEditing opens the Trivy Cloud config file for editing in the default editor specified in the EDITOR environment variable
func OpenConfigForEditing() error {
configPath := getConfigPath()
logger := log.WithPrefix(log.PrefixCloud)
if !fsutils.FileExists(configPath) {
logger.Debug("Trivy Cloud config file does not exist", log.String("config_path", configPath))
defaultConfig.Save()
configPath = getConfigPath()
}
editor := getEditCommand()
cmd := exec.Command(editor, configPath)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func getEditCommand() string {
editor := os.Getenv("EDITOR")
if editor != "" {
return editor
}
// fallback to notepad for windows or vi for macos/linux
if runtime.GOOS == "windows" {
return "notepad"
}
return "vi"
}

147
pkg/cloud/config_modify.go Normal file
View File

@@ -0,0 +1,147 @@
package cloud
import (
"reflect"
"strings"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
// Set sets a nested field in the Trivy Cloud config
func Set(attribute string, value any) error {
config, err := Load()
if err != nil {
return xerrors.Errorf("failed to load Trivy Cloud config file: %w", err)
}
if err := setNestedField(reflect.ValueOf(config).Elem(), attribute, value); err != nil {
return xerrors.Errorf("failed to set attribute %q: %w", attribute, err)
}
return config.Save()
}
// Unset sets a nested field in the Trivy Cloud config to its default value
func Unset(attribute string) error {
config, err := Load()
if err != nil {
return xerrors.Errorf("failed to load Trivy Cloud config file: %w", err)
}
if err := unsetNestedField(reflect.ValueOf(config).Elem(), attribute); err != nil {
return xerrors.Errorf("failed to unset attribute %q: %w", attribute, err)
}
return config.Save()
}
func unsetNestedField(value reflect.Value, attribute string) error {
field, err := navigateToField(value, attribute)
if err != nil {
return err
}
defaultField, err := navigateToField(reflect.ValueOf(defaultConfig).Elem(), attribute)
if err != nil {
return err
}
field.Set(defaultField)
return nil
}
// Get gets a nested field from the Trivy Cloud config
func Get(attribute string) (any, error) {
return GetWithDefault[any](attribute, nil)
}
// GetWithDefault gets a nested field from the Trivy Cloud config with a default value
func GetWithDefault[T any](attribute string, defaultValue T) (T, error) {
config, err := Load()
if err != nil {
return defaultValue, xerrors.Errorf("failed to load Trivy Cloud config file: %w", err)
}
field, err := navigateToField(reflect.ValueOf(config).Elem(), attribute)
if err != nil {
return defaultValue, xerrors.Errorf("failed to get attribute %q: %w", attribute, err)
}
return field.Interface().(T), nil
}
func setNestedField(v reflect.Value, path string, value any) error {
field, err := navigateToField(v, path)
if err != nil {
return err
}
convertedValue, err := convertToType(value, field.Type())
if err != nil {
return xerrors.Errorf("failed to convert value: %w", err)
}
field.Set(convertedValue)
return nil
}
func convertToType(value any, targetType reflect.Type) (reflect.Value, error) {
val := reflect.ValueOf(value)
if val.Type().AssignableTo(targetType) {
return val, nil
}
targetPtr := reflect.New(targetType) // *T
targetInterface := targetPtr.Interface()
data, err := yaml.Marshal(value)
if err != nil {
return reflect.Value{}, xerrors.Errorf("failed to marshal value: %w", err)
}
if err := yaml.Unmarshal(data, targetInterface); err != nil {
return reflect.Value{}, xerrors.Errorf("failed to decode into %v: %w", targetType, err)
}
return targetPtr.Elem(), nil
}
func navigateToField(v reflect.Value, path string) (reflect.Value, error) {
parts := strings.Split(path, ".")
if len(parts) == 0 {
return reflect.Value{}, xerrors.New("empty attribute path")
}
for i, part := range parts {
fieldName := yamlTagToFieldName(v, part)
if fieldName == "" {
return reflect.Value{}, xerrors.Errorf("field %q not found in config", part)
}
field := v.FieldByName(fieldName)
if !field.IsValid() {
return reflect.Value{}, xerrors.Errorf("field %q not found", fieldName)
}
if !field.CanSet() {
return reflect.Value{}, xerrors.Errorf("field %q cannot be set", fieldName)
}
if i == len(parts)-1 {
return field, nil
}
v = field
}
return reflect.Value{}, xerrors.New("unexpected end of path")
}
func yamlTagToFieldName(v reflect.Value, yamlTag string) string {
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("yaml")
tagName := strings.Split(tag, ",")[0]
if tagName == yamlTag {
return field.Name
}
}
return ""
}

View File

@@ -0,0 +1,244 @@
package cloud
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zalando/go-keyring"
)
func TestSet(t *testing.T) {
tests := []struct {
name string
configToSet map[string]any
expected *Config
expectedError string
}{
{
name: "success with valid config",
configToSet: map[string]any{"server.scanning.enabled": true},
expected: &Config{Api: Api{URL: "https://api.trivy.dev"}, Server: Server{URL: "https://scan.trivy.dev", Scanning: Scanning{Enabled: true, UploadResults: false, SecretConfig: false, MisconfigConfig: false}}, IsLoggedIn: false, Token: ""},
expectedError: "",
},
{
name: "success with valid config using off for a boolean",
configToSet: map[string]any{"server.scanning.enabled": "on"},
expected: &Config{Api: Api{URL: "https://api.trivy.dev"}, Server: Server{URL: "https://scan.trivy.dev", Scanning: Scanning{Enabled: true, UploadResults: false, SecretConfig: false, MisconfigConfig: false}}, IsLoggedIn: false, Token: ""},
expectedError: "",
},
{
name: "error with invalid config",
configToSet: map[string]any{"server.scanning.foo": false},
expected: &Config{Api: Api{URL: "https://api.trivy.dev"}, Server: Server{URL: "https://scan.trivy.dev", Scanning: Scanning{Enabled: false, UploadResults: false, SecretConfig: false, MisconfigConfig: false}}, IsLoggedIn: false, Token: ""},
expectedError: "field \"foo\" not found in config",
},
{
name: "error when setting boolean with yessir",
configToSet: map[string]any{"server.scanning.enabled": "yessir"},
expected: &Config{Api: Api{URL: "https://api.trivy.dev"}, Server: Server{URL: "https://scan.trivy.dev", Scanning: Scanning{Enabled: false, UploadResults: false, SecretConfig: false, MisconfigConfig: false}}, IsLoggedIn: false, Token: ""},
expectedError: "cannot unmarshal !!str `yessir` into bool",
},
{
name: "error when setting boolean with invalid value",
configToSet: map[string]any{"server.scanning.enabled": "invalid"},
expected: &Config{Api: Api{URL: "https://api.trivy.dev"}, Server: Server{URL: "https://scan.trivy.dev", Scanning: Scanning{Enabled: false, UploadResults: false, SecretConfig: false, MisconfigConfig: false}}, IsLoggedIn: false, Token: ""},
expectedError: "cannot unmarshal !!str `invalid` into bool",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("XDG_DATA_HOME", tempDir)
keyring.MockInit()
defer keyring.DeleteAll(ServiceName)
defer Clear()
for key, value := range tt.configToSet {
err := Set(key, value)
if tt.expectedError != "" {
require.ErrorContains(t, err, tt.expectedError)
return
}
require.NoError(t, err)
}
config, err := Load()
require.NoError(t, err)
assert.Equal(t, tt.expected, config)
})
}
}
func TestGet(t *testing.T) {
tests := []struct {
name string
primeToken bool
setupConfig *Config
attribute string
defaultValue any
expected any
expectedError string
}{
{
name: "success with default config",
setupConfig: nil,
attribute: "server.scanning.enabled",
defaultValue: false,
expected: false,
expectedError: "",
},
{
name: "success with custom config",
primeToken: true,
setupConfig: &Config{
Token: "test",
Server: Server{
URL: "https://example.com",
Scanning: Scanning{
Enabled: false,
UploadResults: true,
SecretConfig: false,
MisconfigConfig: true,
},
},
Api: Api{URL: "https://api.example.com"},
},
attribute: "server.scanning.enabled",
defaultValue: false,
expected: false,
expectedError: "",
},
{
name: "error with invalid attribute",
setupConfig: nil,
attribute: "server.scanning.foo",
defaultValue: true,
expected: true,
expectedError: "field \"foo\" not found in config",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("XDG_DATA_HOME", tempDir)
keyring.MockInit()
defer keyring.DeleteAll(ServiceName)
defer Clear()
if tt.primeToken {
// add the key so the custom config isn't overwritten
require.NoError(t, keyring.Set(ServiceName, TokenKey, tt.setupConfig.Token))
}
if tt.setupConfig != nil {
err := tt.setupConfig.Save()
require.NoError(t, err)
}
value, err := GetWithDefault(tt.attribute, tt.defaultValue)
if tt.expectedError != "" {
require.ErrorContains(t, err, tt.expectedError)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected, value)
})
}
}
func TestUnset(t *testing.T) {
tests := []struct {
name string
primeToken bool
setupConfig *Config
attribute string
expectedValue any
expectedError string
}{
{
name: "success with default config",
setupConfig: defaultConfig,
attribute: "server.scanning.enabled",
expectedValue: false,
expectedError: "",
},
{
name: "success with custom config",
setupConfig: &Config{
Token: "test",
Server: Server{
URL: "https://example.com",
Scanning: Scanning{
Enabled: false,
UploadResults: true,
SecretConfig: false,
MisconfigConfig: true,
},
},
Api: Api{URL: "https://api.example.com"},
},
attribute: "server.scanning.enabled",
expectedValue: false,
expectedError: "",
},
{
name: "success with custom url reset",
setupConfig: &Config{
Token: "test",
Server: Server{
URL: "https://example.com",
Scanning: Scanning{
Enabled: false,
UploadResults: true,
SecretConfig: false,
MisconfigConfig: true,
},
},
Api: Api{URL: "https://api.custom.com"},
},
attribute: "api.url",
expectedValue: "https://api.trivy.dev",
expectedError: "",
},
{
name: "error with invalid attribute",
setupConfig: defaultConfig,
attribute: "server.scanning.foo",
expectedValue: true,
expectedError: "field \"foo\" not found in config",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("XDG_DATA_HOME", tempDir)
keyring.MockInit()
defer keyring.DeleteAll(ServiceName)
defer Clear()
if tt.primeToken {
// prime the token so it doesn't get overwritten
require.NoError(t, keyring.Set(ServiceName, TokenKey, tt.setupConfig.Token))
}
require.NoError(t, tt.setupConfig.Save())
err := Unset(tt.attribute)
if tt.expectedError != "" {
require.ErrorContains(t, err, tt.expectedError)
return
}
require.NoError(t, err)
value, err := Get(tt.attribute)
require.NoError(t, err)
assert.Equal(t, tt.expectedValue, value)
})
}
}

View File

@@ -27,17 +27,25 @@ func TestSave(t *testing.T) {
{
name: "config with all fields",
config: &Config{
Token: "test-token-123",
ServerURL: "https://example.com",
ApiURL: "https://api.example.com",
Token: "test-token-123",
Server: Server{
URL: "https://example.com",
},
Api: Api{
URL: "https://api.example.com",
},
},
wantErr: false,
},
{
name: "config without token",
config: &Config{
ServerURL: "https://example.com",
ApiURL: "https://api.example.com",
Server: Server{
URL: "https://example.com",
},
Api: Api{
URL: "https://api.example.com",
},
},
wantErr: false,
},
@@ -67,7 +75,7 @@ func TestSave(t *testing.T) {
assert.Equal(t, tt.config, config)
configPath := getConfigPath()
if tt.config.ServerURL != "" || tt.config.ApiURL != "" {
if tt.config.Server.URL != "" || tt.config.Api.URL != "" {
assert.FileExists(t, configPath)
}
})
@@ -102,8 +110,10 @@ func TestClear(t *testing.T) {
if tt.createConfig {
config := &Config{
Token: "testtoken",
ServerURL: "https://example.com",
Token: "testtoken",
Server: Server{
URL: "https://example.com",
},
}
err := config.Save()
require.NoError(t, err)
@@ -153,9 +163,13 @@ func TestLoad(t *testing.T) {
token := "testtoken"
if tt.createConfig {
config := &Config{
Token: token,
ServerURL: "https://example.com",
ApiURL: "https://api.example.com",
Token: token,
Server: Server{
URL: "https://example.com",
},
Api: Api{
URL: "https://api.example.com",
},
}
err := config.Save()
require.NoError(t, err)
@@ -169,8 +183,8 @@ func TestLoad(t *testing.T) {
require.NotNil(t, config)
require.NoError(t, err)
assert.Equal(t, token, config.Token)
assert.Equal(t, "https://example.com", config.ServerURL)
assert.Equal(t, "https://api.example.com", config.ApiURL)
assert.Equal(t, "https://example.com", config.Server.URL)
assert.Equal(t, "https://api.example.com", config.Api.URL)
})
}
}
@@ -184,7 +198,7 @@ func TestVerify(t *testing.T) {
}{
{
name: "success with valid config",
config: &Config{Token: "testtoken", ServerURL: "https://example.com", ApiURL: "https://api.example.com"},
config: &Config{Token: "testtoken", Server: Server{URL: "https://example.com"}, Api: Api{URL: "https://api.example.com"}},
status: http.StatusOK,
wantErr: false,
},
@@ -211,7 +225,7 @@ func TestVerify(t *testing.T) {
}))
defer server.Close()
tt.config.ServerURL = server.URL
tt.config.Server.URL = server.URL
err := tt.config.Verify(context.Background())
if tt.wantErr {
@@ -223,21 +237,60 @@ func TestVerify(t *testing.T) {
}
}
func TestShowConfig(t *testing.T) {
func TestListConfig(t *testing.T) {
tests := []struct {
name string
config *Config
primeToken bool
setupConfig *Config
wantErr string
wantContains []string
}{
{
name: "success with valid config",
config: &Config{Token: "testtoken", ServerURL: "https://example.com", ApiURL: "https://api.example.com"},
name: "success with valid config",
primeToken: true,
setupConfig: &Config{
Token: "testtoken",
Server: Server{
URL: "https://example.com",
Scanning: Scanning{
Enabled: true,
UploadResults: false,
SecretConfig: true,
MisconfigConfig: false,
},
},
Api: Api{URL: "https://api.example.com"},
},
wantContains: []string{
"Trivy Cloud Configuration",
"Trivy Server URL: https://example.com",
"API URL: https://api.example.com",
"Logged In: No",
"Logged In: No",
"Filepath:",
"api.url",
"https://api.example.com",
"server.url",
"https://example.com",
"server.scanning.enabled",
"Enabled",
"server.scanning.upload-results",
"Disabled",
"server.scanning.secret-config",
"server.scanning.misconfig-config",
},
},
{
name: "success with default config",
setupConfig: nil,
wantContains: []string{
"Trivy Cloud Configuration",
"Logged In: No",
"api.url",
DefaultApiUrl,
"server.url",
DefaultTrivyServerUrl,
"server.scanning.enabled",
"server.scanning.upload-results",
"server.scanning.secret-config",
"server.scanning.misconfig-config",
},
},
}
@@ -251,8 +304,13 @@ func TestShowConfig(t *testing.T) {
defer keyring.DeleteAll(ServiceName)
defer Clear()
if tt.config != nil {
err := tt.config.Save()
if tt.primeToken {
// prime the token in the keyring so the custom config doesn't get overwritten
require.NoError(t, keyring.Set(ServiceName, TokenKey, tt.setupConfig.Token))
}
if tt.setupConfig != nil {
err := tt.setupConfig.Save()
require.NoError(t, err)
}
@@ -264,7 +322,7 @@ func TestShowConfig(t *testing.T) {
errChan := make(chan error, 1)
go func() {
errChan <- ShowConfig()
errChan <- ListConfig()
w.Close()
}()

View File

@@ -84,7 +84,7 @@ func (h *CloudPlatformResultsHook) uploadResults(ctx context.Context, jsonReport
}
func (h *CloudPlatformResultsHook) getPresignedUploadUrl(ctx context.Context) (string, error) {
uploadUrl, err := url.JoinPath(h.cloudConfig.ApiURL, presignedUploadUrl)
uploadUrl, err := url.JoinPath(h.cloudConfig.Api.URL, presignedUploadUrl)
if err != nil {
return "", fmt.Errorf("failed to join API URL and presigned upload URL: %w", err)
}

View File

@@ -1476,7 +1476,7 @@ func NewLogoutCommand() *cobra.Command {
func NewCloudCommand() *cobra.Command {
cloudCmd := &cobra.Command{
Use: "cloud [flags]",
Use: "cloud subcommand",
Short: "Control Trivy Cloud platform integration settings",
GroupID: cloud.GroupCloud,
}
@@ -1487,26 +1487,81 @@ func NewCloudCommand() *cobra.Command {
Title: "Trivy Cloud Commands",
})
cloudCmd.AddCommand(
configCmd := &cobra.Command{
Use: "config subcommand",
Short: "Control Trivy Cloud configuration",
GroupID: cloud.GroupCloud,
}
configCmd.AddGroup(&cobra.Group{
ID: cloud.GroupCloud,
Title: "Trivy Cloud Configuration Commands",
})
configCmd.AddCommand(
&cobra.Command{
Use: "edit-config",
Use: "edit",
Short: "Edit Trivy Cloud configuration",
Long: "Edit the Trivy Cloud platform configuration in the default editor specified in the EDITOR environment variable",
Long: "Edit Trivy Cloud platform configuration in the default editor specified in the EDITOR environment variable",
GroupID: cloud.GroupCloud,
RunE: func(_ *cobra.Command, _ []string) error {
return cloud.EditConfig()
},
},
&cobra.Command{
Use: "show-config",
Short: "Show Trivy Cloud configuration",
Long: "Show Trivy Cloud platform configuration in human readable format",
Use: "list",
Short: "List Trivy Cloud configuration",
Long: "List Trivy Cloud platform configuration in human readable format",
GroupID: cloud.GroupCloud,
RunE: func(_ *cobra.Command, _ []string) error {
return cloud.ShowConfig()
return cloud.ListConfig()
},
},
&cobra.Command{
Use: "set [setting] [value]",
Short: "Set Trivy Cloud configuration",
Long: `Set a Trivy Cloud platform setting
Available config settings can be viewed by using the ` + "`trivy cloud config list`" + ` command`,
Example: ` $ trivy cloud config set server.scanning.enabled true
$ trivy cloud config set server.scanning.upload-results false`,
Args: cobra.ExactArgs(2),
GroupID: cloud.GroupCloud,
RunE: func(_ *cobra.Command, args []string) error {
return cloud.SetConfig(args[0], args[1])
},
},
&cobra.Command{
Use: "unset [setting]",
Short: "Unset Trivy Cloud configuration",
Long: `Unset a Trivy Cloud platform configuration and return it to the default setting
Available config settings can be viewed by using the ` + "`trivy cloud config list`" + ` command`,
Example: ` $ trivy cloud config unset server.scanning.enabled
$ trivy cloud config unset server.scanning.upload-results`,
Args: cobra.ExactArgs(1),
GroupID: cloud.GroupCloud,
RunE: func(_ *cobra.Command, args []string) error {
return cloud.UnsetConfig(args[0])
},
},
&cobra.Command{
Use: "get [setting]",
Short: "Get Trivy Cloud configuration",
Long: `Get a Trivy Cloud platform configuration
Available config settings can be viewed by using the ` + "`trivy cloud config list`" + ` command`,
Example: ` $ trivy cloud config get server.scanning.enabled
$ trivy cloud config get server.scanning.upload-results`,
Args: cobra.ExactArgs(1),
GroupID: cloud.GroupCloud,
RunE: func(_ *cobra.Command, args []string) error {
return cloud.GetConfig(args[0])
},
},
)
cloudCmd.AddCommand(configCmd)
return cloudCmd
}

View File

@@ -36,8 +36,8 @@ func Login(ctx context.Context, opts flag.Options) error {
return xerrors.Errorf("failed to load Trivy Cloud config: %w", err)
}
cloudConfig.Token = creds.Token
cloudConfig.ServerURL = opts.CloudOptions.TrivyServerUrl
cloudConfig.ApiURL = opts.CloudOptions.ApiUrl
cloudConfig.Server.URL = opts.CloudOptions.TrivyServerUrl
cloudConfig.Api.URL = opts.CloudOptions.ApiUrl
if err := cloudConfig.Verify(ctx); err != nil {
return xerrors.Errorf("failed to verify Trivy Cloud config: %w", err)
@@ -77,14 +77,14 @@ func CheckTrivyCloudStatus(cmd *cobra.Command) error {
if cloudConfig != nil && cloudConfig.Verify(cmd.Context()) == nil {
logger.Info("Trivy cloud is logged in")
if cloudConfig.ServerScanning {
if cloudConfig.Server.Scanning.Enabled {
logger.Info("Trivy Cloud server scanning is enabled")
os.Setenv("TRIVY_SERVER", cloudConfig.ServerURL)
os.Setenv("TRIVY_SERVER", cloudConfig.Server.URL)
os.Setenv("TRIVY_TOKEN_HEADER", "Authorization")
os.Setenv("TRIVY_TOKEN", fmt.Sprintf("Bearer %s", cloudConfig.Token))
}
if cloudConfig.UploadResults {
if cloudConfig.Server.Scanning.UploadResults {
logger.Info("Trivy Cloud results upload is enabled")
// add hook to upload the results to Trivy Cloud
resultHook := hooks.NewResultsHook(cloudConfig)
@@ -95,10 +95,26 @@ func CheckTrivyCloudStatus(cmd *cobra.Command) error {
return nil
}
func ShowConfig() error {
return cloud.ShowConfig()
func ListConfig() error {
return cloud.ListConfig()
}
func EditConfig() error {
return cloud.OpenConfigForEditing()
}
func SetConfig(attribute string, value any) error {
return cloud.Set(attribute, value)
}
func UnsetConfig(attribute string) error {
return cloud.Unset(attribute)
}
func GetConfig(attribute string) error {
value, err := cloud.Get(attribute)
if err != nil {
return xerrors.Errorf("failed to get Trivy Cloud config: %w", err)
}
fmt.Println(value)
return nil
}

View File

@@ -42,8 +42,12 @@ func TestLogout(t *testing.T) {
if tt.createConfigFile {
config := &cloud.Config{
ServerURL: "https://example.com",
ApiURL: "https://api.example.com",
Server: cloud.Server{
URL: "https://example.com",
},
Api: cloud.Api{
URL: "https://api.example.com",
},
}
err := config.Save()
require.NoError(t, err)
@@ -134,8 +138,8 @@ func TestLogin(t *testing.T) {
config, err := cloud.Load()
require.NoError(t, err)
require.Equal(t, tt.token, config.Token)
require.Equal(t, server.URL, config.ServerURL)
require.Equal(t, server.URL+"/api", config.ApiURL)
require.Equal(t, server.URL, config.Server.URL)
require.Equal(t, server.URL+"/api", config.Api.URL)
})
}
}