mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
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:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
29
docs/docs/references/configuration/cli/trivy_auth.md
Normal file
29
docs/docs/references/configuration/cli/trivy_auth.md
Normal 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
|
||||
|
||||
41
docs/docs/references/configuration/cli/trivy_auth_login.md
Normal file
41
docs/docs/references/configuration/cli/trivy_auth_login.md
Normal 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
|
||||
|
||||
38
docs/docs/references/configuration/cli/trivy_auth_logout.md
Normal file
38
docs/docs/references/configuration/cli/trivy_auth_logout.md
Normal 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -461,6 +461,9 @@ registry:
|
||||
# Same as '--password'
|
||||
password: []
|
||||
|
||||
# Same as '--password-stdin'
|
||||
password-stdin: false
|
||||
|
||||
# Same as '--registry-token'
|
||||
token: ""
|
||||
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
109
pkg/commands/auth/run.go
Normal 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
|
||||
}
|
||||
142
pkg/commands/auth/run_test.go
Normal file
142
pkg/commands/auth/run_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user