mirror of
https://github.com/aquasecurity/trivy.git
synced 2026-01-31 13:53:14 +08:00
feat(cli): Add trivy cloud suppport (#9637)
This commit is contained in:
@@ -44,11 +44,14 @@ trivy [global flags] command [flags] target
|
||||
### SEE ALSO
|
||||
|
||||
* [trivy clean](trivy_clean.md) - Remove cached files
|
||||
* [trivy cloud](trivy_cloud.md) - Control Trivy Cloud platform integration settings
|
||||
* [trivy config](trivy_config.md) - Scan config files for misconfigurations
|
||||
* [trivy convert](trivy_convert.md) - Convert Trivy JSON report into a different format
|
||||
* [trivy filesystem](trivy_filesystem.md) - Scan local filesystem
|
||||
* [trivy image](trivy_image.md) - Scan a container image
|
||||
* [trivy kubernetes](trivy_kubernetes.md) - [EXPERIMENTAL] Scan kubernetes cluster
|
||||
* [trivy login](trivy_login.md) - Log in to the Trivy Cloud platform
|
||||
* [trivy logout](trivy_logout.md) - Log out of Trivy Cloud platform
|
||||
* [trivy module](trivy_module.md) - Manage modules
|
||||
* [trivy plugin](trivy_plugin.md) - Manage plugins
|
||||
* [trivy registry](trivy_registry.md) - Manage registry authentication
|
||||
|
||||
29
docs/docs/references/configuration/cli/trivy_cloud.md
Normal file
29
docs/docs/references/configuration/cli/trivy_cloud.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## trivy cloud
|
||||
|
||||
Control Trivy Cloud platform integration settings
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for cloud
|
||||
```
|
||||
|
||||
### 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](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
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
## trivy cloud edit-config
|
||||
|
||||
Edit Trivy Cloud configuration
|
||||
|
||||
### Synopsis
|
||||
|
||||
Edit the Trivy Cloud platform configuration in the default editor specified in the EDITOR environment variable
|
||||
|
||||
```
|
||||
trivy cloud edit-config [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for edit-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
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
## trivy cloud show-config
|
||||
|
||||
Show Trivy Cloud configuration
|
||||
|
||||
### Synopsis
|
||||
|
||||
Show Trivy Cloud platform configuration in human readable format
|
||||
|
||||
```
|
||||
trivy cloud show-config [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for show-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
|
||||
|
||||
45
docs/docs/references/configuration/cli/trivy_login.md
Normal file
45
docs/docs/references/configuration/cli/trivy_login.md
Normal file
@@ -0,0 +1,45 @@
|
||||
## trivy login
|
||||
|
||||
Log in to the Trivy Cloud platform
|
||||
|
||||
### Synopsis
|
||||
|
||||
Log in to the Trivy Cloud platform to enable scanning of images and repositories in the cloud using the token retrieved from the Trivy Cloud platform
|
||||
|
||||
```
|
||||
trivy login [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
# Log in to the Trivy Cloud platform
|
||||
$ trivy login --token <token>
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--api-url string API URL for Trivy Cloud platform (default "https://api.trivy.dev")
|
||||
-h, --help help for login
|
||||
--token string Token used to athenticate with Trivy Cloud platform
|
||||
--trivy-server-url string Trivy Server URL for Trivy Cloud platform (default "https://scan.trivy.dev")
|
||||
```
|
||||
|
||||
### 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](trivy.md) - Unified security scanner
|
||||
|
||||
31
docs/docs/references/configuration/cli/trivy_logout.md
Normal file
31
docs/docs/references/configuration/cli/trivy_logout.md
Normal file
@@ -0,0 +1,31 @@
|
||||
## trivy logout
|
||||
|
||||
Log out of Trivy Cloud platform
|
||||
|
||||
```
|
||||
trivy logout [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for logout
|
||||
```
|
||||
|
||||
### 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](trivy.md) - Unified security scanner
|
||||
|
||||
5
go.mod
5
go.mod
@@ -130,7 +130,10 @@ require (
|
||||
modernc.org/sqlite v1.39.0
|
||||
)
|
||||
|
||||
require github.com/zalando/go-keyring v0.2.6
|
||||
|
||||
require (
|
||||
al.essio.dev/pkg/shellescape v1.5.1 // indirect
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 // indirect
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250606164443-9d1800bf4ccc.1 // indirect
|
||||
@@ -222,6 +225,7 @@ require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/danieljoos/wincred v1.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
@@ -279,6 +283,7 @@ require (
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.15.23 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gofrs/uuid v4.3.1+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -1,3 +1,5 @@
|
||||
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
|
||||
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 h1:f6miF8tK6H+Ktad24WpnNfpHO75GRGk0rhJ1mxPXqgA=
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1/go.mod h1:rvbyamNtvJ4o3ExeCmaG5/6iHnu0vy0E+UQ+Ph0om8s=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250613105001-9f2d3c737feb.1 h1:AUL6VF5YWL01j/1H/DQbPUSDkEwYqwVCNw7yhbpOxSQ=
|
||||
@@ -697,6 +699,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI=
|
||||
github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
|
||||
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM=
|
||||
github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js=
|
||||
@@ -1309,8 +1313,8 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms=
|
||||
github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
|
||||
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
|
||||
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
|
||||
github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
|
||||
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
|
||||
github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
|
||||
|
||||
@@ -169,11 +169,17 @@ nav:
|
||||
- CLI:
|
||||
- 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
|
||||
- Config: docs/references/configuration/cli/trivy_config.md
|
||||
- Convert: docs/references/configuration/cli/trivy_convert.md
|
||||
- Filesystem: docs/references/configuration/cli/trivy_filesystem.md
|
||||
- Image: docs/references/configuration/cli/trivy_image.md
|
||||
- Kubernetes: docs/references/configuration/cli/trivy_kubernetes.md
|
||||
- Login: docs/references/configuration/cli/trivy_login.md
|
||||
- Logout: docs/references/configuration/cli/trivy_logout.md
|
||||
- Module:
|
||||
- Module: docs/references/configuration/cli/trivy_module.md
|
||||
- Module Install: docs/references/configuration/cli/trivy_module_install.md
|
||||
|
||||
221
pkg/cloud/config.go
Normal file
221
pkg/cloud/config.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/zalando/go-keyring"
|
||||
"golang.org/x/xerrors"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
|
||||
xhttp "github.com/aquasecurity/trivy/pkg/x/http"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceName = "trivy-cloud"
|
||||
TokenKey = "token"
|
||||
DefaultApiUrl = "https://api.trivy.dev"
|
||||
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"`
|
||||
|
||||
IsLoggedIn bool `yaml:"-"`
|
||||
Token string `yaml:"-"`
|
||||
}
|
||||
|
||||
var defaultConfig = &Config{
|
||||
ServerScanning: true,
|
||||
UploadResults: true,
|
||||
ServerURL: DefaultTrivyServerUrl,
|
||||
ApiURL: DefaultApiUrl,
|
||||
}
|
||||
|
||||
func getConfigPath() string {
|
||||
configFileName := fmt.Sprintf("%s.yaml", ServiceName)
|
||||
return filepath.Join(fsutils.TrivyHomeDir(), configFileName)
|
||||
}
|
||||
|
||||
func (c *Config) Save() error {
|
||||
if c.Token == "" && c.ServerURL == "" && c.ApiURL == "" {
|
||||
return xerrors.New("no config to save, required fields are token, server url, and api url")
|
||||
}
|
||||
|
||||
if err := keyring.Set(ServiceName, TokenKey, c.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := getConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configYaml, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
yamlWithFrontmatter := append([]byte("---\n"), configYaml...)
|
||||
return os.WriteFile(configPath, yamlWithFrontmatter, 0o600)
|
||||
}
|
||||
|
||||
func Clear() error {
|
||||
if err := keyring.Delete(ServiceName, TokenKey); err != nil {
|
||||
if !errors.Is(err, keyring.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
configPath := getConfigPath()
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
logger := log.WithPrefix(log.PrefixCloud)
|
||||
var config Config
|
||||
configPath := getConfigPath()
|
||||
yamlData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
logger.Debug("No cloud config file found")
|
||||
return defaultConfig, nil
|
||||
}
|
||||
if err := yaml.Unmarshal(yamlData, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := keyring.Get(ServiceName, TokenKey)
|
||||
if err != nil {
|
||||
if !errors.Is(err, keyring.ErrNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
logger.Debug("No token found in keychain")
|
||||
return defaultConfig, nil
|
||||
}
|
||||
|
||||
config.Token = token
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Verify verifies the Trivy Cloud token and server URL and sets the global cloud config
|
||||
// if the token is valid, the IsLoggedIn field is set to true and the global loggedIn variable is set to true
|
||||
func (c *Config) Verify(ctx context.Context) error {
|
||||
if c.Token == "" {
|
||||
return xerrors.New("no token provided for verification")
|
||||
}
|
||||
|
||||
if c.ServerURL == "" {
|
||||
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")
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to join server URL and verify path: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, http.NoBody)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to create verification request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to verify token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return xerrors.Errorf("failed to verify token: received status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
logger.Debug("Trivy Cloud token verified successfully")
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// 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 {
|
||||
cloudConfig, err := Load()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to load Trivy Cloud config file: %w", err)
|
||||
}
|
||||
|
||||
var loggedIn bool
|
||||
if cloudConfig.Verify(context.Background()) == nil {
|
||||
loggedIn = true
|
||||
} else {
|
||||
loggedIn = false
|
||||
}
|
||||
|
||||
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())
|
||||
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"
|
||||
|
||||
}
|
||||
287
pkg/cloud/config_test.go
Normal file
287
pkg/cloud/config_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *Config
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty config",
|
||||
config: &Config{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "config with all fields",
|
||||
config: &Config{
|
||||
Token: "test-token-123",
|
||||
ServerURL: "https://example.com",
|
||||
ApiURL: "https://api.example.com",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "config without token",
|
||||
config: &Config{
|
||||
ServerURL: "https://example.com",
|
||||
ApiURL: "https://api.example.com",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("XDG_DATA_HOME", tempDir)
|
||||
|
||||
keyring.MockInit()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer keyring.DeleteAll(ServiceName)
|
||||
defer Clear()
|
||||
|
||||
err := tt.config.Save()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
config, err := Load()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.config, config)
|
||||
|
||||
configPath := getConfigPath()
|
||||
if tt.config.ServerURL != "" || tt.config.ApiURL != "" {
|
||||
assert.FileExists(t, configPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
createConfig bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "success when nothing to clear",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "success when there is config to clear",
|
||||
createConfig: true,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("XDG_DATA_HOME", tempDir)
|
||||
|
||||
keyring.MockInit()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer keyring.DeleteAll(ServiceName)
|
||||
defer Clear()
|
||||
|
||||
if tt.createConfig {
|
||||
config := &Config{
|
||||
Token: "testtoken",
|
||||
ServerURL: "https://example.com",
|
||||
}
|
||||
err := config.Save()
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := getConfigPath()
|
||||
assert.FileExists(t, configPath)
|
||||
}
|
||||
|
||||
err := Clear()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
configPath := getConfigPath()
|
||||
assert.NoFileExists(t, configPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
createConfig bool
|
||||
expectDefault bool
|
||||
}{
|
||||
{
|
||||
name: "success when there is config to load",
|
||||
createConfig: true,
|
||||
expectDefault: false,
|
||||
},
|
||||
{
|
||||
name: "error when there is no config to load",
|
||||
expectDefault: true,
|
||||
},
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("XDG_DATA_HOME", tempDir)
|
||||
|
||||
keyring.MockInit()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer keyring.DeleteAll(ServiceName)
|
||||
defer Clear()
|
||||
|
||||
token := "testtoken"
|
||||
if tt.createConfig {
|
||||
config := &Config{
|
||||
Token: token,
|
||||
ServerURL: "https://example.com",
|
||||
ApiURL: "https://api.example.com",
|
||||
}
|
||||
err := config.Save()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
config, err := Load()
|
||||
if tt.expectDefault {
|
||||
assert.Equal(t, defaultConfig, config)
|
||||
return
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *Config
|
||||
status int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "success with valid config",
|
||||
config: &Config{Token: "testtoken", ServerURL: "https://example.com", ApiURL: "https://api.example.com"},
|
||||
status: http.StatusOK,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "error with invalid config",
|
||||
config: &Config{},
|
||||
status: http.StatusUnauthorized,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("XDG_DATA_HOME", tempDir)
|
||||
|
||||
keyring.MockInit()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer keyring.DeleteAll(ServiceName)
|
||||
defer Clear()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "/verify", r.URL.Path)
|
||||
w.WriteHeader(tt.status)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tt.config.ServerURL = server.URL
|
||||
|
||||
err := tt.config.Verify(context.Background())
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *Config
|
||||
wantErr string
|
||||
wantContains []string
|
||||
}{
|
||||
{
|
||||
name: "success with valid config",
|
||||
config: &Config{Token: "testtoken", ServerURL: "https://example.com", ApiURL: "https://api.example.com"},
|
||||
wantContains: []string{
|
||||
"Trivy Cloud Configuration",
|
||||
"Trivy Server URL: https://example.com",
|
||||
"API URL: https://api.example.com",
|
||||
"Logged In: No",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("XDG_DATA_HOME", tempDir)
|
||||
|
||||
keyring.MockInit()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer keyring.DeleteAll(ServiceName)
|
||||
defer Clear()
|
||||
|
||||
if tt.config != nil {
|
||||
err := tt.config.Save()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
|
||||
originalStdout := os.Stdout
|
||||
os.Stdout = w
|
||||
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
errChan <- ShowConfig()
|
||||
w.Close()
|
||||
}()
|
||||
|
||||
output, _ := io.ReadAll(r)
|
||||
os.Stdout = originalStdout
|
||||
|
||||
err = <-errChan
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
outputStr := string(output)
|
||||
for _, want := range tt.wantContains {
|
||||
assert.Contains(t, outputStr, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
119
pkg/cloud/hooks/report_hook.go
Normal file
119
pkg/cloud/hooks/report_hook.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/cloud"
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
xhttp "github.com/aquasecurity/trivy/pkg/x/http"
|
||||
)
|
||||
|
||||
const (
|
||||
presignedUploadUrl = "/trivy-reports/upload-url"
|
||||
)
|
||||
|
||||
type CloudPlatformResultsHook struct {
|
||||
name string
|
||||
cloudConfig *cloud.Config
|
||||
client *http.Client
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewResultsHook(cloudCfg *cloud.Config) *CloudPlatformResultsHook {
|
||||
return &CloudPlatformResultsHook{
|
||||
name: "Trivy Cloud Results Hook",
|
||||
cloudConfig: cloudCfg,
|
||||
client: xhttp.Client(),
|
||||
logger: log.WithPrefix(log.PrefixCloud),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CloudPlatformResultsHook) Name() string {
|
||||
return h.name
|
||||
}
|
||||
|
||||
// PreReport is not going go to be called so we return nil
|
||||
func (h *CloudPlatformResultsHook) PreReport(_ context.Context, _ *types.Report, _ flag.Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *CloudPlatformResultsHook) PostReport(ctx context.Context, report *types.Report, _ flag.Options) error {
|
||||
h.logger.Debug("PostReport called with report")
|
||||
jsonReport, err := json.MarshalIndent(report, "", " ")
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to marshal report to JSON: %w", err)
|
||||
}
|
||||
|
||||
return h.uploadResults(ctx, jsonReport)
|
||||
}
|
||||
|
||||
func (h *CloudPlatformResultsHook) uploadResults(ctx context.Context, jsonReport []byte) error {
|
||||
uploadUrl, err := h.getPresignedUploadUrl(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get presigned upload URL: %w", err)
|
||||
}
|
||||
|
||||
// create a new request to upload the results
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl, bytes.NewBuffer(jsonReport))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create upload request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload results: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to upload results: received status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
h.logger.Info("Report uploaded successfully to Trivy Cloud")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *CloudPlatformResultsHook) getPresignedUploadUrl(ctx context.Context) (string, error) {
|
||||
uploadUrl, err := url.JoinPath(h.cloudConfig.ApiURL, presignedUploadUrl)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to join API URL and presigned upload URL: %w", err)
|
||||
}
|
||||
h.logger.Debug("Requesting result upload URL", log.String("uploadUrl", uploadUrl))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uploadUrl, http.NoBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+h.cloudConfig.Token)
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get upload URL: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed to get upload URL: %w", err)
|
||||
}
|
||||
|
||||
// read the upload URL from the response
|
||||
var uploadResponse struct {
|
||||
UploadURL string `json:"uploadUrl"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&uploadResponse); err != nil {
|
||||
return "", xerrors.Errorf("failed to decode upload URL response: %w", err)
|
||||
}
|
||||
|
||||
return uploadResponse.UploadURL, nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/aquasecurity/trivy/pkg/commands/artifact"
|
||||
"github.com/aquasecurity/trivy/pkg/commands/auth"
|
||||
"github.com/aquasecurity/trivy/pkg/commands/clean"
|
||||
"github.com/aquasecurity/trivy/pkg/commands/cloud"
|
||||
"github.com/aquasecurity/trivy/pkg/commands/convert"
|
||||
"github.com/aquasecurity/trivy/pkg/commands/server"
|
||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||
@@ -81,6 +82,10 @@ func NewApp() *cobra.Command {
|
||||
ID: groupUtility,
|
||||
Title: "Utility Commands",
|
||||
},
|
||||
&cobra.Group{
|
||||
ID: cloud.GroupCloud,
|
||||
Title: "Trivy Cloud Commands",
|
||||
},
|
||||
)
|
||||
rootCmd.SetCompletionCommandGroupID(groupUtility)
|
||||
rootCmd.SetHelpCommandGroupID(groupUtility)
|
||||
@@ -102,6 +107,9 @@ func NewApp() *cobra.Command {
|
||||
NewCleanCommand(globalFlags),
|
||||
NewRegistryCommand(globalFlags),
|
||||
NewVEXCommand(globalFlags),
|
||||
NewLoginCommand(globalFlags),
|
||||
NewLogoutCommand(),
|
||||
NewCloudCommand(),
|
||||
)
|
||||
|
||||
if plugins := loadPluginCommands(); len(plugins) > 0 {
|
||||
@@ -209,7 +217,7 @@ func NewRootCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
|
||||
// Initialize logger
|
||||
log.InitLogger(opts.Debug, opts.Quiet)
|
||||
|
||||
return nil
|
||||
return cloud.CheckTrivyCloudStatus(cmd)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
flags := flag.Flags{globalFlags}
|
||||
@@ -1414,6 +1422,94 @@ func NewVEXCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewLoginCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
|
||||
loginFlags := &flag.Flags{
|
||||
globalFlags,
|
||||
flag.NewCloudFlagGroup(),
|
||||
}
|
||||
|
||||
loginCmd := &cobra.Command{
|
||||
Use: "login [flags]",
|
||||
Short: "Log in to the Trivy Cloud platform",
|
||||
Long: "Log in to the Trivy Cloud platform to enable scanning of images and repositories in the cloud using the token retrieved from the Trivy Cloud platform",
|
||||
GroupID: cloud.GroupCloud,
|
||||
Args: cobra.NoArgs,
|
||||
Example: ` # Log in to the Trivy Cloud platform
|
||||
$ trivy login --token <token>`,
|
||||
PreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if err := loginFlags.Bind(cmd); err != nil {
|
||||
return xerrors.Errorf("flag bind error: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := loginFlags.Bind(cmd); err != nil {
|
||||
return xerrors.Errorf("flag bind error: %w", err)
|
||||
}
|
||||
cloudOptions, err := loginFlags.ToOptions(args)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("flag error: %w", err)
|
||||
}
|
||||
return cloud.Login(cmd.Context(), cloudOptions)
|
||||
},
|
||||
}
|
||||
|
||||
loginFlags.AddFlags(loginCmd)
|
||||
loginCmd.SetFlagErrorFunc(flagErrorFunc)
|
||||
|
||||
return loginCmd
|
||||
}
|
||||
|
||||
func NewLogoutCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "logout",
|
||||
Short: "Log out of Trivy Cloud platform",
|
||||
GroupID: cloud.GroupCloud,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cloud.Logout()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewCloudCommand() *cobra.Command {
|
||||
cloudCmd := &cobra.Command{
|
||||
Use: "cloud [flags]",
|
||||
Short: "Control Trivy Cloud platform integration settings",
|
||||
GroupID: cloud.GroupCloud,
|
||||
}
|
||||
|
||||
// add the group the sub commands so they don't check the login status
|
||||
cloudCmd.AddGroup(&cobra.Group{
|
||||
ID: cloud.GroupCloud,
|
||||
Title: "Trivy Cloud Commands",
|
||||
})
|
||||
|
||||
cloudCmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "edit-config",
|
||||
Short: "Edit Trivy Cloud configuration",
|
||||
Long: "Edit the 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",
|
||||
GroupID: cloud.GroupCloud,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cloud.ShowConfig()
|
||||
},
|
||||
},
|
||||
)
|
||||
return cloudCmd
|
||||
}
|
||||
|
||||
func NewVersionCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
|
||||
var versionFormat string
|
||||
cmd := &cobra.Command{
|
||||
|
||||
104
pkg/commands/cloud/run.go
Normal file
104
pkg/commands/cloud/run.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/cloud"
|
||||
"github.com/aquasecurity/trivy/pkg/cloud/hooks"
|
||||
"github.com/aquasecurity/trivy/pkg/extension"
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
)
|
||||
|
||||
const GroupCloud = "cloud"
|
||||
|
||||
// Login performs a login to the Trivy Cloud Server service using the provided credentials.
|
||||
func Login(ctx context.Context, opts flag.Options) error {
|
||||
creds := opts.CloudOptions.LoginCredentials
|
||||
if creds.Token == "" {
|
||||
return xerrors.New("token is required for Trivy Cloud login")
|
||||
}
|
||||
if opts.CloudOptions.TrivyServerUrl == "" {
|
||||
return xerrors.New("trivy server url is required for Trivy Cloud login")
|
||||
}
|
||||
if opts.CloudOptions.ApiUrl == "" {
|
||||
return xerrors.New("api url is required for Trivy Cloud login")
|
||||
}
|
||||
|
||||
// load the existing config or get the default
|
||||
cloudConfig, err := cloud.Load()
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
if err := cloudConfig.Verify(ctx); err != nil {
|
||||
return xerrors.Errorf("failed to verify Trivy Cloud config: %w", err)
|
||||
}
|
||||
|
||||
if err := cloudConfig.Save(); err != nil {
|
||||
return xerrors.Errorf("failed to save Trivy Cloud config: %w", err)
|
||||
}
|
||||
|
||||
log.WithPrefix(log.PrefixCloud).Info("Trivy Cloud login successful")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout removes the Trivy cloud configuration from both keychain and config file.
|
||||
func Logout() error {
|
||||
if err := cloud.Clear(); err != nil {
|
||||
return xerrors.Errorf("failed to clear Trivy Cloud configuration: %w", err)
|
||||
}
|
||||
|
||||
log.WithPrefix(log.PrefixCloud).Info("Logged out of Trivy cloud and removed configuration")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckTrivyCloudStatus checks if the Trivy Cloud configuration file exists and verifies the token.
|
||||
// If the token is valid, it sets the environment variables TRIVY_SERVER and TRIVY_TOKEN.
|
||||
func CheckTrivyCloudStatus(cmd *cobra.Command) error {
|
||||
if cmd.GroupID == GroupCloud {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger := log.WithPrefix(log.PrefixCloud)
|
||||
cloudConfig, err := cloud.Load()
|
||||
if err != nil {
|
||||
logger.Error("Failed to load Trivy Cloud config file", log.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if cloudConfig != nil && cloudConfig.Verify(cmd.Context()) == nil {
|
||||
logger.Info("Trivy cloud is logged in")
|
||||
if cloudConfig.ServerScanning {
|
||||
logger.Info("Trivy Cloud server scanning is enabled")
|
||||
os.Setenv("TRIVY_SERVER", cloudConfig.ServerURL)
|
||||
os.Setenv("TRIVY_TOKEN_HEADER", "Authorization")
|
||||
os.Setenv("TRIVY_TOKEN", fmt.Sprintf("Bearer %s", cloudConfig.Token))
|
||||
}
|
||||
|
||||
if cloudConfig.UploadResults {
|
||||
logger.Info("Trivy Cloud results upload is enabled")
|
||||
// add hook to upload the results to Trivy Cloud
|
||||
resultHook := hooks.NewResultsHook(cloudConfig)
|
||||
extension.RegisterHook(resultHook)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ShowConfig() error {
|
||||
return cloud.ShowConfig()
|
||||
}
|
||||
|
||||
func EditConfig() error {
|
||||
return cloud.OpenConfigForEditing()
|
||||
}
|
||||
141
pkg/commands/cloud/run_test.go
Normal file
141
pkg/commands/cloud/run_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zalando/go-keyring"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/cloud"
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
)
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
createConfigFile bool
|
||||
}{
|
||||
{
|
||||
name: "successful logout when the config file exists",
|
||||
createConfigFile: true,
|
||||
},
|
||||
{
|
||||
name: "successful logout when the config file does not exist",
|
||||
createConfigFile: false,
|
||||
},
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("XDG_DATA_HOME", tempDir)
|
||||
|
||||
keyring.MockInit()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
defer keyring.DeleteAll(cloud.ServiceName)
|
||||
defer cloud.Clear()
|
||||
cloud.Clear()
|
||||
|
||||
if tt.createConfigFile {
|
||||
config := &cloud.Config{
|
||||
ServerURL: "https://example.com",
|
||||
ApiURL: "https://api.example.com",
|
||||
}
|
||||
err := config.Save()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err := Logout()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
serverResponse int
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "successful login with valid token",
|
||||
token: "valid-token-123",
|
||||
serverResponse: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "login fails with empty token",
|
||||
token: "",
|
||||
serverResponse: http.StatusOK,
|
||||
wantErr: "token is required for Trivy Cloud login",
|
||||
},
|
||||
{
|
||||
name: "login fails with server error",
|
||||
token: "valid-token-123",
|
||||
serverResponse: http.StatusUnauthorized,
|
||||
wantErr: "failed to verify token: received status code 401",
|
||||
},
|
||||
{
|
||||
name: "login fails with server internal error",
|
||||
token: "valid-token-123",
|
||||
serverResponse: http.StatusInternalServerError,
|
||||
wantErr: "failed to verify token: received status code 500",
|
||||
},
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("XDG_DATA_HOME", tempDir)
|
||||
|
||||
keyring.MockInit()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer keyring.DeleteAll(cloud.ServiceName)
|
||||
|
||||
defer cloud.Clear()
|
||||
cloud.Clear()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "/verify", r.URL.Path)
|
||||
|
||||
if tt.token != "" {
|
||||
expectedAuth := "Bearer " + tt.token
|
||||
assert.Equal(t, expectedAuth, r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
w.WriteHeader(tt.serverResponse)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
opts := flag.Options{
|
||||
CloudOptions: flag.CloudOptions{
|
||||
LoginCredentials: flag.CloudLoginCredentials{
|
||||
Token: tt.token,
|
||||
},
|
||||
ApiUrl: server.URL + "/api",
|
||||
TrivyServerUrl: server.URL,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := Login(ctx, opts)
|
||||
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
config "github.com/aquasecurity/trivy/pkg/config/aws"
|
||||
awsConfig "github.com/aquasecurity/trivy/pkg/config/aws"
|
||||
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
)
|
||||
@@ -21,7 +21,7 @@ type AMI struct {
|
||||
func newAMI(imageID string, storage Storage, region, endpoint string) (*AMI, error) {
|
||||
// TODO: propagate context
|
||||
ctx := context.TODO()
|
||||
cfg, err := config.LoadDefaultAWSConfig(ctx, region, endpoint)
|
||||
cfg, err := awsConfig.LoadDefaultAWSConfig(ctx, region, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
75
pkg/flag/cloud_flags.go
Normal file
75
pkg/flag/cloud_flags.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package flag
|
||||
|
||||
import "github.com/aquasecurity/trivy/pkg/cloud"
|
||||
|
||||
var (
|
||||
CloudTokenFlag = Flag[string]{
|
||||
Name: "token",
|
||||
ConfigName: "cloud.token",
|
||||
Usage: "Token used to athenticate with Trivy Cloud platform",
|
||||
}
|
||||
|
||||
CloudApiUrlFlag = Flag[string]{
|
||||
Name: "api-url",
|
||||
ConfigName: "cloud.api-url",
|
||||
Default: cloud.DefaultApiUrl,
|
||||
Usage: "API URL for Trivy Cloud platform",
|
||||
}
|
||||
|
||||
CloudTrivyServerUrlFlag = Flag[string]{
|
||||
Name: "trivy-server-url",
|
||||
ConfigName: "cloud.trivy_server_url",
|
||||
Default: cloud.DefaultTrivyServerUrl,
|
||||
Usage: "Trivy Server URL for Trivy Cloud platform",
|
||||
}
|
||||
)
|
||||
|
||||
type CloudFlagGroup struct {
|
||||
CloudToken *Flag[string]
|
||||
CloudApiUrl *Flag[string]
|
||||
CloudTrivyServerUrl *Flag[string]
|
||||
}
|
||||
|
||||
func NewCloudFlagGroup() *CloudFlagGroup {
|
||||
return &CloudFlagGroup{
|
||||
CloudToken: CloudTokenFlag.Clone(),
|
||||
CloudApiUrl: CloudApiUrlFlag.Clone(),
|
||||
CloudTrivyServerUrl: CloudTrivyServerUrlFlag.Clone(),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *CloudFlagGroup) Name() string {
|
||||
return "Trivy Cloud"
|
||||
}
|
||||
|
||||
func (f *CloudFlagGroup) Flags() []Flagger {
|
||||
return []Flagger{
|
||||
f.CloudToken,
|
||||
f.CloudApiUrl,
|
||||
f.CloudTrivyServerUrl,
|
||||
}
|
||||
}
|
||||
|
||||
// CloudLoginCredentials is the credentials used to authenticate with Trivy Cloud platform
|
||||
// In the future this would likely have more information stored for refresh tokens, etc
|
||||
type CloudLoginCredentials struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
type CloudOptions struct {
|
||||
LoginCredentials CloudLoginCredentials
|
||||
ApiUrl string
|
||||
TrivyServerUrl string
|
||||
}
|
||||
|
||||
// ToOptions converts the flags to options
|
||||
func (f *CloudFlagGroup) ToOptions(opts *Options) error {
|
||||
opts.CloudOptions = CloudOptions{
|
||||
LoginCredentials: CloudLoginCredentials{
|
||||
Token: f.CloudToken.Value(),
|
||||
},
|
||||
ApiUrl: f.CloudApiUrl.Value(),
|
||||
TrivyServerUrl: f.CloudTrivyServerUrl.Value(),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -391,6 +391,7 @@ type Options struct {
|
||||
RemoteOptions
|
||||
RepoOptions
|
||||
ReportOptions
|
||||
CloudOptions
|
||||
ScanOptions
|
||||
SecretOptions
|
||||
VulnerabilityOptions
|
||||
|
||||
@@ -27,6 +27,7 @@ const (
|
||||
PrefixJavaDB = "javadb"
|
||||
PrefixSPDX = "spdx"
|
||||
PrefixCycloneDX = "cyclonedx"
|
||||
PrefixCloud = "cloud"
|
||||
)
|
||||
|
||||
// Logger is an alias of slog.Logger
|
||||
|
||||
Reference in New Issue
Block a user