feat(cli): add trivy auth (#7664)

Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
This commit is contained in:
Teppei Fukuda
2024-10-09 14:31:15 +04:00
committed by GitHub
parent 1f2e91b02b
commit 27117f81d5
23 changed files with 512 additions and 23 deletions

View File

@@ -12,9 +12,9 @@ Trivy_container_scanning:
before_script:
- export TRIVY_VERSION=${TRIVY_VERSION:-v0.19.2}
- apk add --no-cache curl docker-cli
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
- curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin ${TRIVY_VERSION}
- curl -sSL -o /tmp/trivy-gitlab.tpl https://github.com/aquasecurity/trivy/raw/${TRIVY_VERSION}/contrib/gitlab.tpl
- trivy auth login --username "$CI_REGISTRY_USER" --password "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- trivy --exit-code 0 --cache-dir .trivycache/ --no-progress --format template --template "@/tmp/trivy-gitlab.tpl" -o gl-container-scanning-report.json $IMAGE
cache:

View File

@@ -1,13 +1,30 @@
Trivy can download images from a private registry without the need for installing Docker or any other 3rd party tools.
This makes it easy to run within a CI process.
## Credential
To use Trivy with private images, simply install it and provide your credentials:
## Login
You can log in to a private registry using the `trivy auth login` command.
It uses the Docker configuration file (`~/.docker/config.json`) to store the credentials under the hood, and the configuration file path can be configured by `DOCKER_CONFIG` environment variable.
```shell
$ cat ~/my_password.txt | trivy auth login --username foo --password-stdin ghcr.io
$ trivy image ghcr.io/your/private_image
```
## Passing Credentials
You can also provide your credentials when scanning.
```shell
$ TRIVY_USERNAME=YOUR_USERNAME TRIVY_PASSWORD=YOUR_PASSWORD trivy image YOUR_PRIVATE_IMAGE
```
!!! warning
When passing credentials via environment variables or CLI flags, Trivy will attempt to use these credentials for all registries encountered during scanning, regardless of the target registry.
This can potentially lead to unintended credential exposure.
To mitigate this risk:
1. Set credentials cautiously and only when necessary.
2. Prefer using `trivy auth config` to pre-configure credentials with specific registries, which ensures credentials are only sent to appropriate registries.
Trivy also supports providing credentials through CLI flags:
```shell
@@ -17,6 +34,7 @@ $ TRIVY_PASSWORD=YOUR_PASSWORD trivy image --username YOUR_USERNAME YOUR_PRIVATE
!!! warning
The CLI flag `--password` is available, but its use is not recommended for security reasons.
You can also store your credentials in `trivy.yaml`.
For more information, please refer to [the documentation](../../references/configuration/config-file.md).
@@ -35,15 +53,5 @@ In the example above, Trivy attempts to use two pairs of credentials:
Please note that the number of usernames and passwords must be the same.
## docker login
If you have Docker configured locally and have set up the credentials, Trivy can access them.
```shell
$ docker login ghcr.io
Username:
Password:
$ trivy image ghcr.io/your/private_image
```
!!! note
`docker login` can be used with any container runtime, such as Podman.
`--password-stdin` doesn't support comma-separated passwords.

View File

@@ -43,6 +43,7 @@ trivy [global flags] command [flags] target
### SEE ALSO
* [trivy auth](trivy_auth.md) - Authentication
* [trivy clean](trivy_clean.md) - Remove cached files
* [trivy config](trivy_config.md) - Scan config files for misconfigurations
* [trivy convert](trivy_convert.md) - Convert Trivy JSON report into a different format

View File

@@ -0,0 +1,29 @@
## trivy auth
Authentication
### Options
```
-h, --help help for auth
```
### 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 auth login](trivy_auth_login.md) - Log in to a registry
* [trivy auth logout](trivy_auth_logout.md) - Log out of a registry

View File

@@ -0,0 +1,41 @@
## trivy auth login
Log in to a registry
```
trivy auth login SERVER [flags]
```
### Examples
```
# Log in to reg.example.com
cat ~/my_password.txt | trivy auth login --username foo --password-stdin reg.example.com
```
### Options
```
-h, --help help for login
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--password-stdin password from stdin. Comma-separated passwords are not supported.
--username strings username. Comma-separated usernames allowed.
```
### 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 auth](trivy_auth.md) - Authentication

View File

@@ -0,0 +1,38 @@
## trivy auth logout
Log out of a registry
```
trivy auth logout SERVER [flags]
```
### Examples
```
# Log out of reg.example.com
trivy auth logout reg.example.com
```
### 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 auth](trivy_auth.md) - Authentication

View File

@@ -39,6 +39,7 @@ trivy config [flags] DIR
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--password-stdin password from stdin. Comma-separated passwords are not supported.
--redis-ca string redis ca file location, if using redis as cache backend
--redis-cert string redis certificate file location, if using redis as cache backend
--redis-key string redis key file location, if using redis as cache backend

View File

@@ -68,6 +68,7 @@ trivy filesystem [flags] PATH
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--password-stdin password from stdin. Comma-separated passwords are not supported.
--pkg-relationships strings list of package relationships (unknown,root,direct,indirect) (default [unknown,root,direct,indirect])
--pkg-types strings list of package types (os,library) (default [os,library])
--redis-ca string redis ca file location, if using redis as cache backend

View File

@@ -86,6 +86,7 @@ trivy image [flags] IMAGE_NAME
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--password-stdin password from stdin. Comma-separated passwords are not supported.
--pkg-relationships strings list of package relationships (unknown,root,direct,indirect) (default [unknown,root,direct,indirect])
--pkg-types strings list of package types (os,library) (default [os,library])
--platform string set platform in the form os/arch if image is multi-platform capable

View File

@@ -83,6 +83,7 @@ trivy kubernetes [flags] [CONTEXT]
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--password-stdin password from stdin. Comma-separated passwords are not supported.
--pkg-relationships strings list of package relationships (unknown,root,direct,indirect) (default [unknown,root,direct,indirect])
--pkg-types strings list of package types (os,library) (default [os,library])
--qps float specify the maximum QPS to the master from this client (default 5)

View File

@@ -68,6 +68,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--password-stdin password from stdin. Comma-separated passwords are not supported.
--pkg-relationships strings list of package relationships (unknown,root,direct,indirect) (default [unknown,root,direct,indirect])
--pkg-types strings list of package types (os,library) (default [os,library])
--redis-ca string redis ca file location, if using redis as cache backend

View File

@@ -70,6 +70,7 @@ trivy rootfs [flags] ROOTDIR
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
--parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5)
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--password-stdin password from stdin. Comma-separated passwords are not supported.
--pkg-relationships strings list of package relationships (unknown,root,direct,indirect) (default [unknown,root,direct,indirect])
--pkg-types strings list of package types (os,library) (default [os,library])
--redis-ca string redis ca file location, if using redis as cache backend

View File

@@ -48,6 +48,7 @@ trivy sbom [flags] SBOM_PATH
-o, --output string output file name
--output-plugin-arg string [EXPERIMENTAL] output plugin arguments
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--password-stdin password from stdin. Comma-separated passwords are not supported.
--pkg-relationships strings list of package relationships (unknown,root,direct,indirect) (default [unknown,root,direct,indirect])
--pkg-types strings list of package types (os,library) (default [os,library])
--redis-ca string redis ca file location, if using redis as cache backend

View File

@@ -30,6 +30,7 @@ trivy server [flags]
--module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules")
--no-progress suppress progress bar
--password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.
--password-stdin password from stdin. Comma-separated passwords are not supported.
--redis-ca string redis ca file location, if using redis as cache backend
--redis-cert string redis certificate file location, if using redis as cache backend
--redis-key string redis key file location, if using redis as cache backend

View File

@@ -461,6 +461,9 @@ registry:
# Same as '--password'
password: []
# Same as '--password-stdin'
password-stdin: false
# Same as '--registry-token'
token: ""

View File

@@ -297,7 +297,7 @@ Trivy supports registries that comply with the following specifications.
- [Docker Registry HTTP API V2](https://docs.docker.com/registry/spec/api/)
- [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec)
You can configure credentials with `docker login`.
You can configure credentials with `trivy auth login`.
See [here](../advanced/private-registries/index.md) for the detail.
### Tar Files

2
go.mod
View File

@@ -43,6 +43,7 @@ require (
github.com/cheggaaa/pb/v3 v3.1.5
github.com/containerd/containerd v1.7.22
github.com/csaf-poc/csaf_distribution/v3 v3.0.0
github.com/docker/cli v27.2.1+incompatible
github.com/docker/docker v27.3.1+incompatible
github.com/docker/go-connections v0.5.0
github.com/fatih/color v1.17.0
@@ -210,7 +211,6 @@ require (
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/docker/cli v27.2.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect

View File

@@ -117,6 +117,7 @@ type registryOption struct {
Username string
Password string
RegistryToken bool
AuthLogin bool
}
func TestRegistry(t *testing.T) {
@@ -164,7 +165,6 @@ func TestRegistry(t *testing.T) {
imageFile: "testdata/fixtures/images/alpine-310.tar.gz",
os: "alpine 3.10.2",
option: registryOption{
AuthURL: authURL,
Username: authUsername,
Password: authPassword,
},
@@ -183,13 +183,24 @@ func TestRegistry(t *testing.T) {
},
golden: "testdata/alpine-310.json.golden",
},
{
name: "authenticate with 'trivy auth login'",
imageName: "alpine:3.10",
imageFile: "testdata/fixtures/images/alpine-310.tar.gz",
os: "alpine 3.10.2",
option: registryOption{
Username: authUsername,
Password: authPassword,
AuthLogin: true,
},
golden: "testdata/alpine-310.json.golden",
},
{
name: "amazonlinux 2",
imageName: "amazonlinux:2",
imageFile: "testdata/fixtures/images/amazon-2.tar.gz",
os: "amazon 2 (Karoo)",
option: registryOption{
AuthURL: authURL,
Username: authUsername,
Password: authPassword,
},
@@ -201,7 +212,6 @@ func TestRegistry(t *testing.T) {
imageFile: "testdata/fixtures/images/debian-buster.tar.gz",
os: "debian 10.1",
option: registryOption{
AuthURL: authURL,
Username: authUsername,
Password: authPassword,
},
@@ -226,6 +236,7 @@ func TestRegistry(t *testing.T) {
require.NoError(t, err)
osArgs, err := scan(t, imageRef, baseDir, tt.option)
require.NoError(t, err)
// Run Trivy
runTest(t, osArgs, tt.golden, "", types.FormatJSON, runOptions{
@@ -262,7 +273,7 @@ func scan(t *testing.T, imageRef name.Reference, baseDir string, opt registryOpt
"json",
"--image-src",
"remote",
"--skip-update",
"--skip-db-update",
imageRef.Name(),
}
@@ -273,14 +284,30 @@ func setupEnv(t *testing.T, imageRef name.Reference, baseDir string, opt registr
t.Setenv("TRIVY_INSECURE", "true")
if opt.Username != "" && opt.Password != "" {
if opt.RegistryToken {
switch {
case opt.RegistryToken:
// Get a registry token in advance
token, err := requestRegistryToken(imageRef, baseDir, opt)
if err != nil {
return err
}
t.Setenv("TRIVY_REGISTRY_TOKEN", token)
} else {
case opt.AuthLogin:
t.Setenv("DOCKER_CONFIG", t.TempDir())
err := execute([]string{
"auth",
"login",
"--username",
opt.Username,
"--password",
opt.Password,
"--insecure",
imageRef.Context().RegistryStr(),
})
if err != nil {
return err
}
default:
t.Setenv("TRIVY_USERNAME", opt.Username)
t.Setenv("TRIVY_PASSWORD", opt.Password)
}

View File

@@ -158,6 +158,10 @@ nav:
- Configuration:
- CLI:
- Overview: docs/references/configuration/cli/trivy.md
- Auth:
- Auth: docs/references/configuration/cli/trivy_auth.md
- Auth Login: docs/references/configuration/cli/trivy_auth_login.md
- Auth Logout: docs/references/configuration/cli/trivy_auth_logout.md
- Clean: docs/references/configuration/cli/trivy_clean.md
- Config: docs/references/configuration/cli/trivy_config.md
- Convert: docs/references/configuration/cli/trivy_convert.md

View File

@@ -15,6 +15,7 @@ import (
"github.com/aquasecurity/trivy/pkg/cache"
"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/convert"
"github.com/aquasecurity/trivy/pkg/commands/server"
@@ -99,6 +100,7 @@ func NewApp() *cobra.Command {
NewVersionCommand(globalFlags),
NewVMCommand(globalFlags),
NewCleanCommand(globalFlags),
NewAuthCommand(globalFlags),
NewVEXCommand(globalFlags),
)
@@ -1233,6 +1235,62 @@ func NewCleanCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
return cmd
}
func NewAuthCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
cmd := &cobra.Command{
Use: "auth [flags]",
GroupID: groupUtility,
Short: "Authentication",
SilenceErrors: true,
SilenceUsage: true,
}
loginFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
}
loginFlags.RegistryFlagGroup.RegistryToken = nil // disable '--registry-token'
loginCmd := &cobra.Command{
Use: "login SERVER",
Short: "Log in to a registry",
SilenceErrors: true,
SilenceUsage: true,
Example: ` # Log in to reg.example.com
cat ~/my_password.txt | trivy auth login --username foo --password-stdin reg.example.com`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []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 {
loginOpts, err := loginFlags.ToOptions(args)
if err != nil {
return xerrors.Errorf("flag error: %w", err)
}
return auth.Login(cmd.Context(), args[0], loginOpts)
},
}
logoutCmd := &cobra.Command{
Use: "logout SERVER",
Short: "Log out of a registry",
SilenceErrors: true,
SilenceUsage: true,
Example: ` # Log out of reg.example.com
trivy auth logout reg.example.com`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return auth.Logout(cmd.Context(), args[0])
},
}
loginFlags.AddFlags(loginCmd)
cmd.AddCommand(loginCmd, logoutCmd)
cmd.SetFlagErrorFunc(flagErrorFunc)
return cmd
}
func NewVEXCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
vexFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,

109
pkg/commands/auth/run.go Normal file
View File

@@ -0,0 +1,109 @@
package auth
import (
"context"
"net/http"
"os"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/types"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/flag"
"github.com/aquasecurity/trivy/pkg/log"
)
func Login(ctx context.Context, registry string, opts flag.Options) error {
if len(opts.Credentials) == 0 {
return xerrors.New("username and password required")
} else if len(opts.Credentials) > 1 {
return xerrors.New("multiple credentials are not allowed")
}
reg, err := parseRegistry(registry, opts)
if err != nil {
return xerrors.Errorf("failed to parse registry: %w", err)
}
serverAddress := reg.Name()
// Validate the credential
_, err = transport.NewWithContext(ctx, reg, &authn.Basic{
Username: opts.Credentials[0].Username,
Password: opts.Credentials[0].Password,
}, httpTransport(opts), []string{reg.Scope(transport.PullScope)})
if err != nil {
return xerrors.Errorf("failed to authenticate: %w", err)
}
cf, err := config.Load(os.Getenv("DOCKER_CONFIG"))
if err != nil {
return xerrors.Errorf("failed to load docker config: %w", err)
}
creds := cf.GetCredentialsStore(serverAddress)
if serverAddress == name.DefaultRegistry {
serverAddress = authn.DefaultAuthKey
}
if err := creds.Store(types.AuthConfig{
ServerAddress: serverAddress,
Username: opts.Credentials[0].Username,
Password: opts.Credentials[0].Password,
}); err != nil {
return xerrors.Errorf("failed to store credentials: %w", err)
}
if err := cf.Save(); err != nil {
return xerrors.Errorf("failed to save docker config: %w", err)
}
log.Info("Login succeeded", log.FilePath(cf.Filename), log.String("username", opts.Credentials[0].Username))
return nil
}
func Logout(_ context.Context, registry string) error {
reg, err := parseRegistry(registry, flag.Options{})
if err != nil {
return xerrors.Errorf("failed to parse registry: %w", err)
}
serverAddress := reg.Name()
cf, err := config.Load(os.Getenv("DOCKER_CONFIG"))
if err != nil {
return xerrors.Errorf("failed to load docker config: %w", err)
}
creds := cf.GetCredentialsStore(serverAddress)
if serverAddress == name.DefaultRegistry {
serverAddress = authn.DefaultAuthKey
}
if err := creds.Erase(serverAddress); err != nil {
return xerrors.Errorf("failed to delete credentials: %w", err)
}
if err := cf.Save(); err != nil {
return xerrors.Errorf("failed to save docker config: %w", err)
}
log.Info("Logged out", log.FilePath(cf.Filename))
return nil
}
func parseRegistry(registry string, opts flag.Options) (name.Registry, error) {
var nameOpts []name.Option
if opts.Insecure {
nameOpts = append(nameOpts, name.Insecure)
}
reg, err := name.NewRegistry(registry, nameOpts...)
if err != nil {
return name.Registry{}, xerrors.Errorf("failed to parse registry: %w", err)
}
return reg, nil
}
func httpTransport(opts flag.Options) *http.Transport {
tr := remote.DefaultTransport.(*http.Transport).Clone()
if opts.Insecure {
tr.TLSClientConfig.InsecureSkipVerify = true
}
return tr
}

View File

@@ -0,0 +1,142 @@
package auth_test
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
testauth "github.com/aquasecurity/testdocker/auth"
"github.com/aquasecurity/testdocker/registry"
"github.com/aquasecurity/trivy/pkg/commands/auth"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/flag"
)
func TestLogin(t *testing.T) {
type args struct {
registry string
opts flag.Options
}
tests := []struct {
name string
args args
wantErr string
}{
{
name: "single credential",
args: args{
opts: flag.Options{
RegistryOptions: flag.RegistryOptions{
Credentials: []types.Credential{
{
Username: "user",
Password: "pass",
},
},
},
},
},
},
{
name: "multiple credentials",
args: args{
opts: flag.Options{
RegistryOptions: flag.RegistryOptions{
Credentials: []types.Credential{
{
Username: "user1",
Password: "pass1",
},
{
Username: "user2",
Password: "pass2",
},
},
},
},
},
wantErr: "multiple credentials are not allowed",
},
{
name: "no credentials",
args: args{
registry: "auth.test",
opts: flag.Options{},
},
wantErr: "username and password required",
},
{
name: "invalid registry",
args: args{
registry: "aaa://invalid.test",
opts: flag.Options{
RegistryOptions: flag.RegistryOptions{
Credentials: []types.Credential{
{
Username: "user",
Password: "pass",
},
},
},
},
},
wantErr: "registries must be valid RFC 3986 URI authorities",
},
}
tr := registry.NewDockerRegistry(registry.Option{
Auth: testauth.Auth{
User: "user",
Password: "pass",
},
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set the DOCKER_CONFIG environment variable to a temporary directory
// so that the test does not interfere with the user's configuration.
t.Setenv("DOCKER_CONFIG", filepath.Join(t.TempDir(), "config.json"))
reg := lo.Ternary(tt.args.registry == "", strings.TrimPrefix(tr.URL, "http://"), tt.args.registry)
err := auth.Login(context.Background(), reg, tt.args.opts)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
})
}
}
func TestLogout(t *testing.T) {
// Set the DOCKER_CONFIG environment variable to a temporary directory
// so that the test does not interfere with the user's configuration.
tmpDir := t.TempDir()
t.Setenv("DOCKER_CONFIG", tmpDir)
t.Run("success", func(t *testing.T) {
configFile := filepath.Join(tmpDir, "config.json")
err := os.WriteFile(configFile, []byte(`{"auths": {"auth.test": {"auth": "dXNlcjpwYXNz"}}}`), 0600)
require.NoError(t, err)
err = auth.Logout(context.Background(), "auth.test")
require.NoError(t, err)
b, err := os.ReadFile(configFile)
require.NoError(t, err)
require.JSONEq(t, `{"auths": {}}`, string(b))
})
t.Run("not found", func(t *testing.T) {
err := auth.Logout(context.Background(), "notfound.test")
require.NoError(t, err) // Return an error if "credsStore" is "osxkeychain".
})
t.Run("invalid registry", func(t *testing.T) {
err := auth.Logout(context.Background(), "aaa://invalid.test")
require.ErrorContains(t, err, "registries must be valid RFC 3986 URI authorities")
})
}

View File

@@ -1,6 +1,8 @@
package flag
import (
"io"
"os"
"strings"
"golang.org/x/xerrors"
@@ -19,6 +21,11 @@ var (
ConfigName: "registry.password",
Usage: "password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.",
}
PasswordStdinFlag = Flag[bool]{
Name: "password-stdin",
ConfigName: "registry.password-stdin",
Usage: "password from stdin. Comma-separated passwords are not supported.",
}
RegistryTokenFlag = Flag[string]{
Name: "registry-token",
ConfigName: "registry.token",
@@ -29,6 +36,7 @@ var (
type RegistryFlagGroup struct {
Username *Flag[[]string]
Password *Flag[[]string]
PasswordStdin *Flag[bool]
RegistryToken *Flag[string]
}
@@ -41,6 +49,7 @@ func NewRegistryFlagGroup() *RegistryFlagGroup {
return &RegistryFlagGroup{
Username: UsernameFlag.Clone(),
Password: PasswordFlag.Clone(),
PasswordStdin: PasswordStdinFlag.Clone(),
RegistryToken: RegistryTokenFlag.Clone(),
}
}
@@ -53,6 +62,7 @@ func (f *RegistryFlagGroup) Flags() []Flagger {
return []Flagger{
f.Username,
f.Password,
f.PasswordStdin,
f.RegistryToken,
}
}
@@ -65,8 +75,19 @@ func (f *RegistryFlagGroup) ToOptions() (RegistryOptions, error) {
var credentials []types.Credential
users := f.Username.Value()
passwords := f.Password.Value()
if f.PasswordStdin.Value() {
if len(passwords) != 0 {
return RegistryOptions{}, xerrors.New("'--password' and '--password-stdin' can't be used at the same time")
}
contents, err := io.ReadAll(os.Stdin)
if err != nil {
return RegistryOptions{}, xerrors.Errorf("failed to read from stdin: %w", err)
}
// "--password-stdin" doesn't support comma-separated passwords
passwords = []string{strings.TrimRight(string(contents), "\r\n")}
}
if len(users) != len(passwords) {
return RegistryOptions{}, xerrors.New("the length of usernames and passwords must match")
return RegistryOptions{}, xerrors.New("the number of usernames and passwords must match")
}
for i, user := range users {
credentials = append(credentials, types.Credential{