refactor(flag): improve flag system architecture and extensibility (#8718)

Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
This commit is contained in:
Teppei Fukuda
2025-04-11 12:47:43 +04:00
committed by GitHub
parent e25de25262
commit 4a38d0121b
28 changed files with 387 additions and 576 deletions

View File

@@ -211,25 +211,26 @@ func NewRootCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
return err
}
globalOptions, err := globalFlags.ToOptions()
flags := flag.Flags{globalFlags}
opts, err := flags.ToOptions(args)
if err != nil {
return err
}
// Initialize logger
log.InitLogger(globalOptions.Debug, globalOptions.Quiet)
log.InitLogger(opts.Debug, opts.Quiet)
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
globalOptions, err := globalFlags.ToOptions()
flags := flag.Flags{globalFlags}
opts, err := flags.ToOptions(args)
if err != nil {
return err
}
if globalOptions.ShowVersion {
if opts.ShowVersion {
// Customize version output
return showVersion(globalOptions.CacheDir, versionFormat, cmd.OutOrStdout())
return showVersion(opts.CacheDir, versionFormat, cmd.OutOrStdout())
} else {
return cmd.Help()
}
@@ -255,27 +256,30 @@ func NewImageCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
compliance.Values = []string{types.ComplianceDockerCIS160}
reportFlagGroup.Compliance = compliance // override usage as the accepted values differ for each subcommand.
imageFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
ImageFlagGroup: flag.NewImageFlagGroup(), // container image specific
LicenseFlagGroup: flag.NewLicenseFlagGroup(),
MisconfFlagGroup: flag.NewMisconfFlagGroup(),
ModuleFlagGroup: flag.NewModuleFlagGroup(),
PackageFlagGroup: flag.NewPackageFlagGroup(),
RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
RegoFlagGroup: flag.NewRegoFlagGroup(),
ReportFlagGroup: reportFlagGroup,
ScanFlagGroup: flag.NewScanFlagGroup(),
SecretFlagGroup: flag.NewSecretFlagGroup(),
VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(),
}
packageFlagGroup := flag.NewPackageFlagGroup()
packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
imageFlags.PackageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
imageFlags.MisconfFlagGroup.CloudformationParamVars = nil // disable '--cf-params'
imageFlags.MisconfFlagGroup.TerraformTFVars = nil // disable '--tf-vars'
misconfFlagGroup := flag.NewMisconfFlagGroup()
misconfFlagGroup.CloudformationParamVars = nil // disable '--cf-params'
misconfFlagGroup.TerraformTFVars = nil // disable '--tf-vars'
imageFlags := flag.Flags{
globalFlags,
flag.NewCacheFlagGroup(),
flag.NewDBFlagGroup(),
flag.NewImageFlagGroup(), // container image specific flags
flag.NewLicenseFlagGroup(),
misconfFlagGroup,
flag.NewModuleFlagGroup(),
packageFlagGroup,
flag.NewClientFlags(),
flag.NewRegistryFlagGroup(),
flag.NewRegoFlagGroup(),
reportFlagGroup,
flag.NewScanFlagGroup(),
flag.NewSecretFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
}
cmd := &cobra.Command{
Use: "image [flags] IMAGE_NAME",
@@ -335,26 +339,29 @@ func NewImageCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
}
func NewFilesystemCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
fsFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
LicenseFlagGroup: flag.NewLicenseFlagGroup(),
MisconfFlagGroup: flag.NewMisconfFlagGroup(),
ModuleFlagGroup: flag.NewModuleFlagGroup(),
PackageFlagGroup: flag.NewPackageFlagGroup(),
RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
RegoFlagGroup: flag.NewRegoFlagGroup(),
ReportFlagGroup: flag.NewReportFlagGroup(),
ScanFlagGroup: flag.NewScanFlagGroup(),
SecretFlagGroup: flag.NewSecretFlagGroup(),
VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(),
}
cacheFlagGroup := flag.NewCacheFlagGroup()
cacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) // Use memory cache by default
fsFlags.CacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) // Use memory cache by default
fsFlags.ReportFlagGroup.ReportFormat.Usage = "specify a compliance report format for the output" // @TODO: support --report summary for non compliance reports
fsFlags.ReportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol'
reportFlagGroup := flag.NewReportFlagGroup()
reportFlagGroup.ReportFormat.Usage = "specify a compliance report format for the output" // @TODO: support --report summary for non compliance reports
reportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol'
fsFlags := flag.Flags{
globalFlags,
cacheFlagGroup,
flag.NewDBFlagGroup(),
flag.NewLicenseFlagGroup(),
flag.NewMisconfFlagGroup(),
flag.NewModuleFlagGroup(),
flag.NewPackageFlagGroup(),
flag.NewClientFlags(), // for client/server mode
flag.NewRegistryFlagGroup(),
flag.NewRegoFlagGroup(),
reportFlagGroup,
flag.NewScanFlagGroup(),
flag.NewSecretFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
}
cmd := &cobra.Command{
Use: "filesystem [flags] PATH",
@@ -394,27 +401,33 @@ func NewFilesystemCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
}
func NewRootfsCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
rootfsFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
LicenseFlagGroup: flag.NewLicenseFlagGroup(),
MisconfFlagGroup: flag.NewMisconfFlagGroup(),
ModuleFlagGroup: flag.NewModuleFlagGroup(),
PackageFlagGroup: flag.NewPackageFlagGroup(),
RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
RegoFlagGroup: flag.NewRegoFlagGroup(),
ReportFlagGroup: flag.NewReportFlagGroup(),
ScanFlagGroup: flag.NewScanFlagGroup(),
SecretFlagGroup: flag.NewSecretFlagGroup(),
VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(),
reportFlagGroup := flag.NewReportFlagGroup()
reportFlagGroup.ReportFormat = nil // TODO: support --report summary
reportFlagGroup.Compliance = nil // disable '--compliance'
reportFlagGroup.ReportFormat = nil // disable '--report'
packageFlagGroup := flag.NewPackageFlagGroup()
packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
cacheFlagGroup := flag.NewCacheFlagGroup()
cacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) // Use memory cache by default
rootfsFlags := flag.Flags{
globalFlags,
cacheFlagGroup,
flag.NewDBFlagGroup(),
flag.NewLicenseFlagGroup(),
flag.NewMisconfFlagGroup(),
flag.NewModuleFlagGroup(),
packageFlagGroup,
flag.NewClientFlags(), // for client/server mode
flag.NewRegistryFlagGroup(),
flag.NewRegoFlagGroup(),
reportFlagGroup,
flag.NewScanFlagGroup(),
flag.NewSecretFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
}
rootfsFlags.ReportFlagGroup.ReportFormat = nil // TODO: support --report summary
rootfsFlags.ReportFlagGroup.Compliance = nil // disable '--compliance'
rootfsFlags.ReportFlagGroup.ReportFormat = nil // disable '--report'
rootfsFlags.PackageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
rootfsFlags.CacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) // Use memory cache by default
cmd := &cobra.Command{
Use: "rootfs [flags] ROOTDIR",
@@ -455,28 +468,31 @@ func NewRootfsCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
}
func NewRepositoryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
repoFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
LicenseFlagGroup: flag.NewLicenseFlagGroup(),
MisconfFlagGroup: flag.NewMisconfFlagGroup(),
ModuleFlagGroup: flag.NewModuleFlagGroup(),
PackageFlagGroup: flag.NewPackageFlagGroup(),
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
RegoFlagGroup: flag.NewRegoFlagGroup(),
RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode
ReportFlagGroup: flag.NewReportFlagGroup(),
ScanFlagGroup: flag.NewScanFlagGroup(),
SecretFlagGroup: flag.NewSecretFlagGroup(),
VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(),
RepoFlagGroup: flag.NewRepoFlagGroup(),
}
repoFlags.ReportFlagGroup.ReportFormat = nil // TODO: support --report summary
repoFlags.ReportFlagGroup.Compliance = nil // disable '--compliance'
repoFlags.ReportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol'
reportFlagGroup := flag.NewReportFlagGroup()
reportFlagGroup.ReportFormat = nil // TODO: support --report summary
reportFlagGroup.Compliance = nil // disable '--compliance'
reportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol'
repoFlags.ScanFlagGroup.DistroFlag = nil // `repo` subcommand doesn't support scanning OS packages, so we can disable `--distro`
scanFlagGroup := flag.NewScanFlagGroup()
scanFlagGroup.DistroFlag = nil // repo subcommand doesn't support scanning OS packages
repoFlags := flag.Flags{
globalFlags,
flag.NewCacheFlagGroup(),
flag.NewDBFlagGroup(),
flag.NewLicenseFlagGroup(),
flag.NewMisconfFlagGroup(),
flag.NewModuleFlagGroup(),
flag.NewPackageFlagGroup(),
flag.NewRegistryFlagGroup(),
flag.NewRegoFlagGroup(),
flag.NewClientFlags(), // for client/server mode
reportFlagGroup,
scanFlagGroup,
flag.NewSecretFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
flag.NewRepoFlagGroup(),
}
cmd := &cobra.Command{
Use: "repository [flags] (REPO_PATH | REPO_URL)",
@@ -514,18 +530,20 @@ func NewRepositoryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
}
func NewConvertCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
convertFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
ScanFlagGroup: &flag.ScanFlagGroup{},
ReportFlagGroup: flag.NewReportFlagGroup(),
}
// To display the summary table, we need to enable scanners (to build columns).
// We can't get scanner information from the report (we don't include empty licenses and secrets in the report).
// So we need to ask the user to configure scanners (if needed).
convertFlags.ScanFlagGroup.Scanners = flag.ScannersFlag.Clone()
convertFlags.ScanFlagGroup.Scanners.Default = nil // disable default scanners
convertFlags.ScanFlagGroup.Scanners.Usage = "List of scanners included when generating the json report. Used only for rendering the summary table."
scanFlagGroup := &flag.ScanFlagGroup{
Scanners: flag.ScannersFlag.Clone(),
}
scanFlagGroup.Scanners.Default = nil // disable default scanners
scanFlagGroup.Scanners.Usage = "List of scanners included when generating the json report. Used only for rendering the summary table."
convertFlags := flag.Flags{
globalFlags,
scanFlagGroup,
flag.NewReportFlagGroup(),
}
cmd := &cobra.Command{
Use: "convert [flags] RESULT_JSON",
@@ -575,17 +593,17 @@ func NewClientCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
}
remoteFlags.ServerAddr = &remoteAddr // disable '--server' and enable '--remote' instead.
clientFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
MisconfFlagGroup: flag.NewMisconfFlagGroup(),
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
RegoFlagGroup: flag.NewRegoFlagGroup(),
RemoteFlagGroup: remoteFlags,
ReportFlagGroup: flag.NewReportFlagGroup(),
ScanFlagGroup: flag.NewScanFlagGroup(),
VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(),
clientFlags := flag.Flags{
globalFlags,
flag.NewCacheFlagGroup(),
flag.NewDBFlagGroup(),
flag.NewMisconfFlagGroup(),
flag.NewRegistryFlagGroup(),
flag.NewRegoFlagGroup(),
remoteFlags,
flag.NewReportFlagGroup(),
flag.NewScanFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
}
cmd := &cobra.Command{
@@ -621,19 +639,20 @@ func NewClientCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
}
func NewServerCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
serverFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
ModuleFlagGroup: flag.NewModuleFlagGroup(),
RemoteFlagGroup: flag.NewServerFlags(),
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
}
// The Java DB is not needed for the server mode
dbFlagGroup := flag.NewDBFlagGroup()
dbFlagGroup.DownloadJavaDBOnly = nil // disable '--download-java-db-only'
dbFlagGroup.SkipJavaDBUpdate = nil // disable '--skip-java-db-update'
dbFlagGroup.JavaDBRepositories = nil // disable '--java-db-repository'
// java-db only works on client side.
serverFlags.DBFlagGroup.DownloadJavaDBOnly = nil // disable '--download-java-db-only'
serverFlags.DBFlagGroup.SkipJavaDBUpdate = nil // disable '--skip-java-db-update'
serverFlags.DBFlagGroup.JavaDBRepositories = nil // disable '--java-db-repository'
serverFlags := flag.Flags{
globalFlags,
flag.NewCacheFlagGroup(),
dbFlagGroup,
flag.NewModuleFlagGroup(),
flag.NewServerFlags(),
flag.NewRegistryFlagGroup(),
}
cmd := &cobra.Command{
Use: "server [flags]",
@@ -675,28 +694,31 @@ func NewConfigCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
FilePatterns: flag.FilePatternsFlag.Clone(),
}
reportFlagGroup := flag.NewReportFlagGroup()
reportFlagGroup.DependencyTree = nil // disable '--dependency-tree'
reportFlagGroup.ListAllPkgs = nil // disable '--list-all-pkgs'
reportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol'
reportFlagGroup.ShowSuppressed = nil // disable '--show-suppressed'
reportFlagGroup.ReportFormat.Usage = "specify a compliance report format for the output" // @TODO: support --report summary for non compliance reports
cacheFlagGroup := flag.NewCacheFlagGroup()
cacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory)
configFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
MisconfFlagGroup: flag.NewMisconfFlagGroup(),
ModuleFlagGroup: flag.NewModuleFlagGroup(),
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
RegoFlagGroup: flag.NewRegoFlagGroup(),
K8sFlagGroup: &flag.K8sFlagGroup{
// disable unneeded flags
globalFlags,
cacheFlagGroup,
flag.NewMisconfFlagGroup(),
flag.NewModuleFlagGroup(),
flag.NewRegistryFlagGroup(),
flag.NewRegoFlagGroup(),
&flag.K8sFlagGroup{
// Keep only --k8s-version flag and disable others
K8sVersion: flag.K8sVersionFlag.Clone(),
},
ReportFlagGroup: flag.NewReportFlagGroup(),
ScanFlagGroup: scanFlags,
reportFlagGroup,
scanFlags,
}
configFlags.ReportFlagGroup.DependencyTree = nil // disable '--dependency-tree'
configFlags.ReportFlagGroup.ListAllPkgs = nil // disable '--list-all-pkgs'
configFlags.ReportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol'
configFlags.ReportFlagGroup.ShowSuppressed = nil // disable '--show-suppressed'
configFlags.ReportFlagGroup.ReportFormat.Usage = "specify a compliance report format for the output" // @TODO: support --report summary for non compliance reports
configFlags.CacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory)
cmd := &cobra.Command{
Use: "config [flags] DIR",
Aliases: []string{"conf"},
@@ -738,7 +760,7 @@ func NewConfigCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
func NewPluginCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
var pluginOptions flag.Options
pluginFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
globalFlags,
}
cmd := &cobra.Command{
Use: "plugin subcommand",
@@ -747,7 +769,8 @@ func NewPluginCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
Short: "Manage plugins",
SilenceErrors: true,
SilenceUsage: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var err error
pluginOptions, err = pluginFlags.ToOptions(args)
if err != nil {
return err
@@ -887,8 +910,8 @@ func NewPluginCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
func NewModuleCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
moduleFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
ModuleFlagGroup: flag.NewModuleFlagGroup(),
globalFlags,
flag.NewModuleFlagGroup(),
}
cmd := &cobra.Command{
@@ -998,22 +1021,24 @@ func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
misconfFlagGroup.CloudformationParamVars = nil // disable '--cf-params'
misconfFlagGroup.TerraformTFVars = nil // disable '--tf-vars'
packageFlagGroup := flag.NewPackageFlagGroup()
packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
k8sFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
ImageFlagGroup: imageFlags,
K8sFlagGroup: flag.NewK8sFlagGroup(), // kubernetes-specific flags
MisconfFlagGroup: misconfFlagGroup,
PackageFlagGroup: flag.NewPackageFlagGroup(),
RegoFlagGroup: flag.NewRegoFlagGroup(),
ReportFlagGroup: reportFlagGroup,
ScanFlagGroup: scanFlags,
SecretFlagGroup: flag.NewSecretFlagGroup(),
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(),
globalFlags,
flag.NewCacheFlagGroup(),
flag.NewDBFlagGroup(),
imageFlags,
flag.NewK8sFlagGroup(), // kubernetes-specific flags
misconfFlagGroup,
packageFlagGroup,
flag.NewRegoFlagGroup(),
reportFlagGroup,
scanFlags,
flag.NewSecretFlagGroup(),
flag.NewRegistryFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
}
k8sFlags.PackageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
cmd := &cobra.Command{
Use: "kubernetes [flags] [CONTEXT]",
@@ -1060,19 +1085,29 @@ func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
}
func NewVMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
reportFlagGroup := flag.NewReportFlagGroup()
reportFlagGroup.ReportFormat = nil // disable '--report'
packageFlagGroup := flag.NewPackageFlagGroup()
packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
misconfFlagGroup := flag.NewMisconfFlagGroup()
misconfFlagGroup.CloudformationParamVars = nil // disable '--cf-params'
misconfFlagGroup.TerraformTFVars = nil // disable '--tf-vars'
vmFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
MisconfFlagGroup: flag.NewMisconfFlagGroup(),
ModuleFlagGroup: flag.NewModuleFlagGroup(),
PackageFlagGroup: flag.NewPackageFlagGroup(),
RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode
ReportFlagGroup: flag.NewReportFlagGroup(),
ScanFlagGroup: flag.NewScanFlagGroup(),
SecretFlagGroup: flag.NewSecretFlagGroup(),
VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(),
AWSFlagGroup: &flag.AWSFlagGroup{
globalFlags,
flag.NewCacheFlagGroup(),
flag.NewDBFlagGroup(),
misconfFlagGroup,
flag.NewModuleFlagGroup(),
packageFlagGroup,
flag.NewClientFlags(), // for client/server mode
reportFlagGroup,
flag.NewScanFlagGroup(),
flag.NewSecretFlagGroup(),
flag.NewVulnerabilityFlagGroup(),
&flag.AWSFlagGroup{
Region: &flag.Flag[string]{
Name: "aws-region",
ConfigName: "aws.region",
@@ -1080,10 +1115,6 @@ func NewVMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
},
},
}
vmFlags.ReportFlagGroup.ReportFormat = nil // disable '--report'
vmFlags.PackageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
vmFlags.MisconfFlagGroup.CloudformationParamVars = nil // disable '--cf-params'
vmFlags.MisconfFlagGroup.TerraformTFVars = nil // disable '--tf-vars'
cmd := &cobra.Command{
Use: "vm [flags] VM_IMAGE",
@@ -1148,21 +1179,24 @@ func NewSBOMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
licenseFlagGroup.LicenseFull = nil
licenseFlagGroup.LicenseConfidenceLevel = nil
sbomFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
PackageFlagGroup: flag.NewPackageFlagGroup(),
RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode
RegistryFlagGroup: flag.NewRegistryFlagGroup(), // for DBs in private registries
ReportFlagGroup: reportFlagGroup,
ScanFlagGroup: scanFlagGroup,
VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(),
LicenseFlagGroup: licenseFlagGroup,
}
cacheFlagGroup := flag.NewCacheFlagGroup()
cacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) // Use memory cache by default
sbomFlags.CacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) // Use memory cache by default
sbomFlags.PackageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
packageFlagGroup := flag.NewPackageFlagGroup()
packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps'
sbomFlags := &flag.Flags{
globalFlags,
cacheFlagGroup,
flag.NewDBFlagGroup(),
packageFlagGroup,
flag.NewClientFlags(), // for client/server mode
flag.NewRegistryFlagGroup(), // for DBs in private registries
reportFlagGroup,
scanFlagGroup,
flag.NewVulnerabilityFlagGroup(),
licenseFlagGroup,
}
cmd := &cobra.Command{
Use: "sbom [flags] SBOM_PATH",
@@ -1203,8 +1237,8 @@ func NewSBOMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
func NewCleanCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
cleanFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
CleanFlagGroup: flag.NewCleanFlagGroup(),
globalFlags,
flag.NewCleanFlagGroup(),
}
cmd := &cobra.Command{
Use: "clean [flags]",
@@ -1252,11 +1286,13 @@ func NewRegistryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
SilenceUsage: true,
}
registryFlagGroup := flag.NewRegistryFlagGroup()
registryFlagGroup.RegistryToken = nil
loginFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
RegistryFlagGroup: flag.NewRegistryFlagGroup(),
globalFlags,
registryFlagGroup,
}
loginFlags.RegistryFlagGroup.RegistryToken = nil // disable '--registry-token'
loginCmd := &cobra.Command{
Use: "login SERVER",
Short: "Log in to a registry",
@@ -1300,23 +1336,25 @@ func NewRegistryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
}
func NewVEXCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
vexFlags := &flag.Flags{
GlobalFlagGroup: globalFlags,
vexFlags := flag.Flags{
globalFlags,
}
var vexOptions flag.Options
cmd := &cobra.Command{
Use: "vex subcommand",
GroupID: groupManagement,
Short: "[EXPERIMENTAL] VEX utilities",
SilenceErrors: true,
SilenceUsage: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
cmd.SetContext(log.WithContextPrefix(cmd.Context(), "vex"))
vexOptions, err = vexFlags.ToOptions(args)
opts, err := vexFlags.ToOptions(args)
if err != nil {
return err
}
vexOptions = opts
return nil
},
}
@@ -1393,11 +1431,12 @@ func NewVersionCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
GroupID: groupUtility,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
options, err := globalFlags.ToOptions()
flags := flag.Flags{globalFlags}
opts, err := flags.ToOptions(args)
if err != nil {
return err
}
return showVersion(options.CacheDir, versionFormat, cmd.OutOrStdout())
return showVersion(opts.CacheDir, versionFormat, cmd.OutOrStdout())
},
SilenceErrors: true,
SilenceUsage: true,

View File

@@ -315,9 +315,9 @@ func TestFlags(t *testing.T) {
rootCmd.SetOut(io.Discard)
flags := &flag.Flags{
GlobalFlagGroup: globalFlags,
ReportFlagGroup: flag.NewReportFlagGroup(),
ScanFlagGroup: flag.NewScanFlagGroup(),
globalFlags,
flag.NewReportFlagGroup(),
flag.NewScanFlagGroup(),
}
cmd := &cobra.Command{
Use: "test",

View File

@@ -77,16 +77,14 @@ func (f *AWSFlagGroup) Flags() []Flagger {
}
}
func (f *AWSFlagGroup) ToOptions() (AWSOptions, error) {
if err := parseFlags(f); err != nil {
return AWSOptions{}, err
}
return AWSOptions{
func (f *AWSFlagGroup) ToOptions(opts *Options) error {
opts.AWSOptions = AWSOptions{
Region: f.Region.Value(),
Endpoint: f.Endpoint.Value(),
Services: f.Services.Value(),
SkipServices: f.SkipServices.Value(),
Account: f.Account.Value(),
ARN: f.ARN.Value(),
}, nil
}
return nil
}

View File

@@ -106,17 +106,15 @@ func (fg *CacheFlagGroup) Flags() []Flagger {
}
}
func (fg *CacheFlagGroup) ToOptions() (CacheOptions, error) {
if err := parseFlags(fg); err != nil {
return CacheOptions{}, err
}
return CacheOptions{
func (fg *CacheFlagGroup) ToOptions(opts *Options) error {
opts.CacheOptions = CacheOptions{
ClearCache: fg.ClearCache.Value(),
CacheBackend: fg.CacheBackend.Value(),
CacheTTL: fg.CacheTTL.Value(),
RedisTLS: fg.RedisTLS.Value(),
RedisCACert: fg.RedisCACert.Value(),
RedisCert: fg.RedisCert.Value(),
RedisKey: fg.RedisKey.Value(),
}, nil
}
return nil
}

View File

@@ -78,17 +78,14 @@ func (fg *CleanFlagGroup) Flags() []Flagger {
}
}
func (fg *CleanFlagGroup) ToOptions() (CleanOptions, error) {
if err := parseFlags(fg); err != nil {
return CleanOptions{}, err
}
return CleanOptions{
func (fg *CleanFlagGroup) ToOptions(opts *Options) error {
opts.CleanOptions = CleanOptions{
CleanAll: fg.CleanAll.Value(),
CleanVulnerabilityDB: fg.CleanVulnerabilityDB.Value(),
CleanJavaDB: fg.CleanJavaDB.Value(),
CleanChecksBundle: fg.CleanChecksBundle.Value(),
CleanScanCache: fg.CleanScanCache.Value(),
CleanVEXRepositories: fg.CleanVEXRepositories.Value(),
}, nil
}
return nil
}

View File

@@ -127,31 +127,27 @@ func (f *DBFlagGroup) Flags() []Flagger {
}
}
func (f *DBFlagGroup) ToOptions() (DBOptions, error) {
if err := parseFlags(f); err != nil {
return DBOptions{}, err
}
func (f *DBFlagGroup) ToOptions(opts *Options) error {
skipDBUpdate := f.SkipDBUpdate.Value()
skipJavaDBUpdate := f.SkipJavaDBUpdate.Value()
downloadDBOnly := f.DownloadDBOnly.Value()
downloadJavaDBOnly := f.DownloadJavaDBOnly.Value()
if downloadDBOnly && downloadJavaDBOnly {
return DBOptions{}, xerrors.New("--download-db-only and --download-java-db-only options can not be specified both")
return xerrors.New("--download-db-only and --download-java-db-only options can not be specified both")
}
if downloadDBOnly && skipDBUpdate {
return DBOptions{}, xerrors.New("--skip-db-update and --download-db-only options can not be specified both")
return xerrors.New("--skip-db-update and --download-db-only options can not be specified both")
}
if downloadJavaDBOnly && skipJavaDBUpdate {
return DBOptions{}, xerrors.New("--skip-java-db-update and --download-java-db-only options can not be specified both")
return xerrors.New("--skip-java-db-update and --download-java-db-only options can not be specified both")
}
var dbRepositories, javaDBRepositories []name.Reference
for _, repo := range f.DBRepositories.Value() {
ref, err := parseRepository(repo, db.SchemaVersion)
if err != nil {
return DBOptions{}, xerrors.Errorf("invalid DB repository: %w", err)
return xerrors.Errorf("invalid DB repository: %w", err)
}
dbRepositories = append(dbRepositories, ref)
}
@@ -159,12 +155,12 @@ func (f *DBFlagGroup) ToOptions() (DBOptions, error) {
for _, repo := range f.JavaDBRepositories.Value() {
ref, err := parseRepository(repo, javadb.SchemaVersion)
if err != nil {
return DBOptions{}, xerrors.Errorf("invalid javadb repository: %w", err)
return xerrors.Errorf("invalid javadb repository: %w", err)
}
javaDBRepositories = append(javaDBRepositories, ref)
}
return DBOptions{
opts.DBOptions = DBOptions{
Reset: f.Reset.Value(),
DownloadDBOnly: downloadDBOnly,
SkipDBUpdate: skipDBUpdate,
@@ -173,7 +169,8 @@ func (f *DBFlagGroup) ToOptions() (DBOptions, error) {
NoProgress: f.NoProgress.Value(),
DBRepositories: dbRepositories,
JavaDBRepositories: javaDBRepositories,
}, nil
}
return nil
}
func parseRepository(repo string, dbSchemaVersion int) (name.Reference, error) {

View File

@@ -101,15 +101,15 @@ func TestDBFlagGroup_ToOptions(t *testing.T) {
DBRepositories: flag.DBRepositoryFlag.Clone(),
JavaDBRepositories: flag.JavaDBRepositoryFlag.Clone(),
}
got, err := f.ToOptions()
flags := flag.Flags{f}
got, err := flags.ToOptions(nil)
if tt.wantErr != "" {
require.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.EqualExportedValues(t, tt.want, got)
assert.EqualExportedValues(t, tt.want, got.DBOptions)
// Assert log messages
assert.Equal(t, tt.wantLogs, out.Messages(), tt.name)

View File

@@ -137,17 +137,13 @@ func (f *GlobalFlagGroup) Bind(cmd *cobra.Command) error {
return nil
}
func (f *GlobalFlagGroup) ToOptions() (GlobalOptions, error) {
if err := parseFlags(f); err != nil {
return GlobalOptions{}, err
}
func (f *GlobalFlagGroup) ToOptions(opts *Options) error {
// Keep TRIVY_NON_SSL for backward compatibility
insecure := f.Insecure.Value() || os.Getenv("TRIVY_NON_SSL") != ""
log.Debug("Cache dir", log.String("dir", f.CacheDir.Value()))
return GlobalOptions{
opts.GlobalOptions = GlobalOptions{
ConfigFile: f.ConfigFile.Value(),
ShowVersion: f.ShowVersion.Value(),
Quiet: f.Quiet.Value(),
@@ -156,5 +152,6 @@ func (f *GlobalFlagGroup) ToOptions() (GlobalOptions, error) {
Timeout: f.Timeout.Value(),
CacheDir: f.CacheDir.Value(),
GenerateDefaultConfig: f.GenerateDefaultConfig.Value(),
}, nil
}
return nil
}

View File

@@ -119,16 +119,12 @@ func (f *ImageFlagGroup) Flags() []Flagger {
}
}
func (f *ImageFlagGroup) ToOptions() (ImageOptions, error) {
if err := parseFlags(f); err != nil {
return ImageOptions{}, err
}
func (f *ImageFlagGroup) ToOptions(opts *Options) error {
var platform ftypes.Platform
if p := f.Platform.Value(); p != "" {
pl, err := v1.ParsePlatform(p)
if err != nil {
return ImageOptions{}, xerrors.Errorf("unable to parse platform: %w", err)
return xerrors.Errorf("unable to parse platform: %w", err)
}
if pl.OS == "*" {
pl.OS = "" // Empty OS means any OS
@@ -139,12 +135,12 @@ func (f *ImageFlagGroup) ToOptions() (ImageOptions, error) {
if value := f.MaxImageSize.Value(); value != "" {
parsedSize, err := units.FromHumanSize(value)
if err != nil {
return ImageOptions{}, xerrors.Errorf("invalid max image size %q: %w", value, err)
return xerrors.Errorf("invalid max image size %q: %w", value, err)
}
maxSize = parsedSize
}
return ImageOptions{
opts.ImageOptions = ImageOptions{
Input: f.Input.Value(),
ImageConfigScanners: xstrings.ToTSlice[types.Scanner](f.ImageConfigScanners.Value()),
ScanRemovedPkgs: f.ScanRemovedPkgs.Value(),
@@ -153,5 +149,6 @@ func (f *ImageFlagGroup) ToOptions() (ImageOptions, error) {
PodmanHost: f.PodmanHost.Value(),
ImageSources: xstrings.ToTSlice[ftypes.ImageSource](f.ImageSources.Value()),
MaxImageSize: maxSize,
}, nil
}
return nil
}

View File

@@ -79,13 +79,14 @@ func TestImageFlagGroup_ToOptions(t *testing.T) {
Platform: flag.PlatformFlag.Clone(),
}
got, err := f.ToOptions()
flags := flag.Flags{f}
got, err := flags.ToOptions(nil)
if tt.wantErr != "" {
assert.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.EqualExportedValues(t, tt.want, got)
assert.EqualExportedValues(t, tt.want, got.ImageOptions)
})
}
}

View File

@@ -2,11 +2,11 @@ package flag
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/samber/lo"
"golang.org/x/xerrors"
corev1 "k8s.io/api/core/v1"
)
@@ -173,14 +173,10 @@ func (f *K8sFlagGroup) Flags() []Flagger {
}
}
func (f *K8sFlagGroup) ToOptions() (K8sOptions, error) {
if err := parseFlags(f); err != nil {
return K8sOptions{}, err
}
func (f *K8sFlagGroup) ToOptions(opts *Options) error {
tolerations, err := optionToTolerations(f.Tolerations.Value())
if err != nil {
return K8sOptions{}, err
return err
}
exludeNodeLabels := make(map[string]string)
@@ -188,18 +184,18 @@ func (f *K8sFlagGroup) ToOptions() (K8sOptions, error) {
for _, exludeNodeValue := range exludeNodes {
excludeNodeParts := strings.Split(exludeNodeValue, ":")
if len(excludeNodeParts) != 2 {
return K8sOptions{}, fmt.Errorf("exclude node %s must be a key:value", exludeNodeValue)
return xerrors.Errorf("exclude node %s must be a key:value", exludeNodeValue)
}
exludeNodeLabels[excludeNodeParts[0]] = excludeNodeParts[1]
}
if len(f.ExcludeNamespaces.Value()) > 0 && len(f.IncludeNamespaces.Value()) > 0 {
return K8sOptions{}, errors.New("include-namespaces and exclude-namespaces flags cannot be used together")
return xerrors.New("include-namespaces and exclude-namespaces flags cannot be used together")
}
if len(f.ExcludeKinds.Value()) > 0 && len(f.IncludeKinds.Value()) > 0 {
return K8sOptions{}, errors.New("include-kinds and exclude-kinds flags cannot be used together")
return xerrors.New("include-kinds and exclude-kinds flags cannot be used together")
}
return K8sOptions{
opts.K8sOptions = K8sOptions{
KubeConfig: f.KubeConfig.Value(),
K8sVersion: f.K8sVersion.Value(),
Tolerations: tolerations,
@@ -215,7 +211,8 @@ func (f *K8sFlagGroup) ToOptions() (K8sOptions, error) {
ExcludeNamespaces: f.ExcludeNamespaces.Value(),
IncludeNamespaces: f.IncludeNamespaces.Value(),
Burst: f.Burst.Value(),
}, nil
}
return nil
}
func optionToTolerations(tolerationsOptions []string) ([]corev1.Toleration, error) {

View File

@@ -115,11 +115,7 @@ func (f *LicenseFlagGroup) Flags() []Flagger {
}
}
func (f *LicenseFlagGroup) ToOptions() (LicenseOptions, error) {
if err := parseFlags(f); err != nil {
return LicenseOptions{}, err
}
func (f *LicenseFlagGroup) ToOptions(opts *Options) error {
licenseCategories := make(map[types.LicenseCategory][]string)
licenseCategories[types.CategoryForbidden] = f.LicenseForbidden.Value()
licenseCategories[types.CategoryRestricted] = f.LicenseRestricted.Value()
@@ -128,10 +124,11 @@ func (f *LicenseFlagGroup) ToOptions() (LicenseOptions, error) {
licenseCategories[types.CategoryPermissive] = f.LicensePermissive.Value()
licenseCategories[types.CategoryUnencumbered] = f.LicenseUnencumbered.Value()
return LicenseOptions{
opts.LicenseOptions = LicenseOptions{
LicenseFull: f.LicenseFull.Value(),
IgnoredLicenses: f.IgnoredLicenses.Value(),
LicenseConfidenceLevel: f.LicenseConfidenceLevel.Value(),
LicenseCategories: licenseCategories,
}, nil
}
return nil
}

View File

@@ -204,12 +204,8 @@ func (f *MisconfFlagGroup) Flags() []Flagger {
}
}
func (f *MisconfFlagGroup) ToOptions() (MisconfOptions, error) {
if err := parseFlags(f); err != nil {
return MisconfOptions{}, err
}
return MisconfOptions{
func (f *MisconfFlagGroup) ToOptions(opts *Options) error {
opts.MisconfOptions = MisconfOptions{
IncludeNonFailures: f.IncludeNonFailures.Value(),
ResetChecksBundle: f.ResetChecksBundle.Value(),
ChecksBundleRepository: f.ChecksBundleRepository.Value(),
@@ -225,5 +221,6 @@ func (f *MisconfFlagGroup) ToOptions() (MisconfOptions, error) {
MisconfigScanners: xstrings.ToTSlice[analyzer.Type](f.MisconfigScanners.Value()),
ConfigFileSchemas: f.ConfigFileSchemas.Value(),
RenderCause: xstrings.ToTSlice[types.ConfigType](f.RenderCause.Value()),
}, nil
}
return nil
}

View File

@@ -58,13 +58,10 @@ func (f *ModuleFlagGroup) Flags() []Flagger {
}
}
func (f *ModuleFlagGroup) ToOptions() (ModuleOptions, error) {
if err := parseFlags(f); err != nil {
return ModuleOptions{}, err
}
return ModuleOptions{
func (f *ModuleFlagGroup) ToOptions(opts *Options) error {
opts.ModuleOptions = ModuleOptions{
ModuleDir: f.Dir.Value(),
EnabledModules: f.EnabledModules.Value(),
}, nil
}
return nil
}

View File

@@ -344,6 +344,7 @@ func (f *Flag[T]) BindEnv() error {
type FlagGroup interface {
Name() string
Flags() []Flagger
ToOptions(*Options) error
}
type Flagger interface {
@@ -358,27 +359,7 @@ type Flagger interface {
Bind(cmd *cobra.Command) error
}
type Flags struct {
GlobalFlagGroup *GlobalFlagGroup
AWSFlagGroup *AWSFlagGroup
CacheFlagGroup *CacheFlagGroup
CleanFlagGroup *CleanFlagGroup
DBFlagGroup *DBFlagGroup
ImageFlagGroup *ImageFlagGroup
K8sFlagGroup *K8sFlagGroup
LicenseFlagGroup *LicenseFlagGroup
MisconfFlagGroup *MisconfFlagGroup
ModuleFlagGroup *ModuleFlagGroup
PackageFlagGroup *PackageFlagGroup
RemoteFlagGroup *RemoteFlagGroup
RegistryFlagGroup *RegistryFlagGroup
RegoFlagGroup *RegoFlagGroup
RepoFlagGroup *RepoFlagGroup
ReportFlagGroup *ReportFlagGroup
ScanFlagGroup *ScanFlagGroup
SecretFlagGroup *SecretFlagGroup
VulnerabilityFlagGroup *VulnerabilityFlagGroup
}
type Flags []FlagGroup
// Options holds all the runtime configuration
type Options struct {
@@ -411,15 +392,19 @@ type Options struct {
// outputWriter is not initialized via the CLI.
// It is mainly used for testing purposes or by tools that use Trivy as a library.
outputWriter io.Writer
// args is the arguments passed to the command.
args []string
}
// Align takes consistency of options
func (o *Options) Align(f *Flags) error {
if f.ScanFlagGroup != nil && f.ScanFlagGroup.Scanners != nil {
if scanFlagGroup, ok := findFlagGroup[*ScanFlagGroup](f); ok && scanFlagGroup.Scanners != nil {
o.enableSBOM()
}
if f.PackageFlagGroup != nil && f.PackageFlagGroup.PkgRelationships != nil &&
if packageFlagGroup, ok := findFlagGroup[*PackageFlagGroup](f); ok &&
packageFlagGroup.PkgRelationships != nil &&
slices.Compare(o.PkgRelationships, ftypes.Relationships) != 0 &&
(o.DependencyTree || slices.Contains(types.SupportedSBOMFormats, o.Format) || len(o.VEXSources) != 0) {
return xerrors.Errorf("'--pkg-relationships' cannot be used with '--dependency-tree', '--vex' or SBOM formats")
@@ -601,63 +586,9 @@ func (o *Options) outputPluginWriter(ctx context.Context) (io.Writer, func() err
// groups returns all the flag groups other than global flags
func (f *Flags) groups() []FlagGroup {
var groups []FlagGroup
// This order affects the usage message, so they are sorted by frequency of use.
if f.ScanFlagGroup != nil {
groups = append(groups, f.ScanFlagGroup)
}
if f.ReportFlagGroup != nil {
groups = append(groups, f.ReportFlagGroup)
}
if f.CacheFlagGroup != nil {
groups = append(groups, f.CacheFlagGroup)
}
if f.CleanFlagGroup != nil {
groups = append(groups, f.CleanFlagGroup)
}
if f.DBFlagGroup != nil {
groups = append(groups, f.DBFlagGroup)
}
if f.RegistryFlagGroup != nil {
groups = append(groups, f.RegistryFlagGroup)
}
if f.ImageFlagGroup != nil {
groups = append(groups, f.ImageFlagGroup)
}
if f.VulnerabilityFlagGroup != nil {
groups = append(groups, f.VulnerabilityFlagGroup)
}
if f.MisconfFlagGroup != nil {
groups = append(groups, f.MisconfFlagGroup)
}
if f.ModuleFlagGroup != nil {
groups = append(groups, f.ModuleFlagGroup)
}
if f.SecretFlagGroup != nil {
groups = append(groups, f.SecretFlagGroup)
}
if f.LicenseFlagGroup != nil {
groups = append(groups, f.LicenseFlagGroup)
}
if f.RegoFlagGroup != nil {
groups = append(groups, f.RegoFlagGroup)
}
if f.AWSFlagGroup != nil {
groups = append(groups, f.AWSFlagGroup)
}
if f.K8sFlagGroup != nil {
groups = append(groups, f.K8sFlagGroup)
}
if f.PackageFlagGroup != nil {
groups = append(groups, f.PackageFlagGroup)
}
if f.RemoteFlagGroup != nil {
groups = append(groups, f.RemoteFlagGroup)
}
if f.RepoFlagGroup != nil {
groups = append(groups, f.RepoFlagGroup)
}
return groups
return lo.Filter(*f, func(group FlagGroup, _ int) bool {
return group != nil && group.Name() != "Global"
})
}
func (f *Flags) AddFlags(cmd *cobra.Command) {
@@ -715,141 +646,18 @@ func (f *Flags) Bind(cmd *cobra.Command) error {
// nolint: gocyclo
func (f *Flags) ToOptions(args []string) (Options, error) {
var err error
opts := Options{
AppVersion: app.Version(),
args: args,
}
if f.GlobalFlagGroup != nil {
opts.GlobalOptions, err = f.GlobalFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("global flag error: %w", err)
for _, group := range *f { // Include global flags
if err := parseFlags(group); err != nil {
return Options{}, xerrors.Errorf("unable to parse flags: %w", err)
}
}
if f.AWSFlagGroup != nil {
opts.AWSOptions, err = f.AWSFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("aws flag error: %w", err)
}
}
if f.CacheFlagGroup != nil {
opts.CacheOptions, err = f.CacheFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("cache flag error: %w", err)
}
}
if f.CleanFlagGroup != nil {
opts.CleanOptions, err = f.CleanFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("clean flag error: %w", err)
}
}
if f.DBFlagGroup != nil {
opts.DBOptions, err = f.DBFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("db flag error: %w", err)
}
}
if f.ImageFlagGroup != nil {
opts.ImageOptions, err = f.ImageFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("image flag error: %w", err)
}
}
if f.K8sFlagGroup != nil {
opts.K8sOptions, err = f.K8sFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("k8s flag error: %w", err)
}
}
if f.LicenseFlagGroup != nil {
opts.LicenseOptions, err = f.LicenseFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("license flag error: %w", err)
}
}
if f.MisconfFlagGroup != nil {
opts.MisconfOptions, err = f.MisconfFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("misconfiguration flag error: %w", err)
}
}
if f.ModuleFlagGroup != nil {
opts.ModuleOptions, err = f.ModuleFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("module flag error: %w", err)
}
}
if f.PackageFlagGroup != nil {
opts.PackageOptions, err = f.PackageFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("package flag error: %w", err)
}
}
if f.RegoFlagGroup != nil {
opts.RegoOptions, err = f.RegoFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("rego flag error: %w", err)
}
}
if f.RemoteFlagGroup != nil {
opts.RemoteOptions, err = f.RemoteFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("remote flag error: %w", err)
}
}
if f.RegistryFlagGroup != nil {
opts.RegistryOptions, err = f.RegistryFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("registry flag error: %w", err)
}
}
if f.RepoFlagGroup != nil {
opts.RepoOptions, err = f.RepoFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("repo flag error: %w", err)
}
}
if f.ReportFlagGroup != nil {
opts.ReportOptions, err = f.ReportFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("report flag error: %w", err)
}
}
if f.ScanFlagGroup != nil {
opts.ScanOptions, err = f.ScanFlagGroup.ToOptions(args)
if err != nil {
return Options{}, xerrors.Errorf("scan flag error: %w", err)
}
}
if f.SecretFlagGroup != nil {
opts.SecretOptions, err = f.SecretFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("secret flag error: %w", err)
}
}
if f.VulnerabilityFlagGroup != nil {
opts.VulnerabilityOptions, err = f.VulnerabilityFlagGroup.ToOptions()
if err != nil {
return Options{}, xerrors.Errorf("vulnerability flag error: %w", err)
if err := group.ToOptions(&opts); err != nil {
return Options{}, xerrors.Errorf("unable to convert flags to options: %w", err)
}
}
@@ -935,3 +743,16 @@ func HiddenFlags() []string {
}
return hiddenFlags
}
// findFlagGroup finds a flag group by type T
// Note that Go generics doesn't support methods today.
// cf. https://github.com/golang/go/issues/49085
func findFlagGroup[T FlagGroup](f *Flags) (T, bool) {
for _, group := range *f {
if g, ok := group.(T); ok {
return g, true
}
}
var zero T
return zero, false
}

View File

@@ -69,23 +69,20 @@ func (f *PackageFlagGroup) Flags() []Flagger {
}
}
func (f *PackageFlagGroup) ToOptions() (PackageOptions, error) {
if err := parseFlags(f); err != nil {
return PackageOptions{}, err
}
func (f *PackageFlagGroup) ToOptions(opts *Options) error {
var relationships []ftypes.Relationship
for _, r := range f.PkgRelationships.Value() {
relationship, err := ftypes.NewRelationship(r)
if err != nil {
return PackageOptions{}, err
return err
}
relationships = append(relationships, relationship)
}
return PackageOptions{
opts.PackageOptions = PackageOptions{
IncludeDevDeps: f.IncludeDevDeps.Value(),
PkgTypes: f.PkgTypes.Value(),
PkgRelationships: relationships,
}, nil
}
return nil
}

View File

@@ -76,9 +76,10 @@ func TestPackageFlagGroup_ToOptions(t *testing.T) {
PkgRelationships: flag.PkgRelationshipsFlag.Clone(),
}
got, err := f.ToOptions()
flags := flag.Flags{f}
got, err := flags.ToOptions(nil)
require.NoError(t, err)
assert.EqualExportedValuesf(t, tt.want, got, "PackageFlagGroup")
assert.EqualExportedValues(t, tt.want, got.PackageOptions)
})
}
}

View File

@@ -75,27 +75,23 @@ func (f *RegistryFlagGroup) Flags() []Flagger {
}
}
func (f *RegistryFlagGroup) ToOptions() (RegistryOptions, error) {
if err := parseFlags(f); err != nil {
return RegistryOptions{}, err
}
func (f *RegistryFlagGroup) ToOptions(opts *Options) 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")
return 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)
return 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 number of usernames and passwords must match")
return xerrors.New("the number of usernames and passwords must match")
}
for i, user := range users {
credentials = append(credentials, types.Credential{
@@ -104,9 +100,10 @@ func (f *RegistryFlagGroup) ToOptions() (RegistryOptions, error) {
})
}
return RegistryOptions{
opts.RegistryOptions = RegistryOptions{
Credentials: credentials,
RegistryToken: f.RegistryToken.Value(),
RegistryMirrors: f.RegistryMirrors.Value(),
}, nil
}
return nil
}

View File

@@ -102,17 +102,14 @@ func (f *RegoFlagGroup) Flags() []Flagger {
}
}
func (f *RegoFlagGroup) ToOptions() (RegoOptions, error) {
if err := parseFlags(f); err != nil {
return RegoOptions{}, err
}
return RegoOptions{
func (f *RegoFlagGroup) ToOptions(opts *Options) error {
opts.RegoOptions = RegoOptions{
IncludeDeprecatedChecks: f.IncludeDeprecatedChecks.Value(),
SkipCheckUpdate: f.SkipCheckUpdate.Value(),
Trace: f.Trace.Value(),
CheckPaths: f.CheckPaths.Value(),
DataPaths: f.DataPaths.Value(),
CheckNamespaces: f.CheckNamespaces.Value(),
}, nil
}
return nil
}

View File

@@ -110,11 +110,7 @@ func (f *RemoteFlagGroup) Flags() []Flagger {
}
}
func (f *RemoteFlagGroup) ToOptions() (RemoteOptions, error) {
if err := parseFlags(f); err != nil {
return RemoteOptions{}, err
}
func (f *RemoteFlagGroup) ToOptions(opts *Options) error {
serverAddr := f.ServerAddr.Value()
customHeaders := splitCustomHeaders(f.CustomHeaders.Value())
listen := f.Listen.Value()
@@ -140,14 +136,15 @@ func (f *RemoteFlagGroup) ToOptions() (RemoteOptions, error) {
customHeaders.Set(tokenHeader, token)
}
return RemoteOptions{
opts.RemoteOptions = RemoteOptions{
Token: token,
TokenHeader: tokenHeader,
PathPrefix: f.PathPrefix.Value(),
ServerAddr: serverAddr,
CustomHeaders: customHeaders,
Listen: listen,
}, nil
}
return nil
}
func splitCustomHeaders(headers []string) http.Header {

View File

@@ -110,9 +110,10 @@ func TestRemoteFlagGroup_ToOptions(t *testing.T) {
Token: flag.ServerTokenFlag.Clone(),
TokenHeader: flag.ServerTokenHeaderFlag.Clone(),
}
got, err := f.ToOptions()
flags := flag.Flags{f}
got, err := flags.ToOptions(nil)
require.NoError(t, err)
assert.Equalf(t, tt.want, got, "ToOptions()")
assert.Equal(t, tt.want, got.RemoteOptions)
// Assert log messages
assert.Equal(t, tt.wantLogs, out.Messages(), tt.name)

View File

@@ -50,14 +50,11 @@ func (f *RepoFlagGroup) Flags() []Flagger {
}
}
func (f *RepoFlagGroup) ToOptions() (RepoOptions, error) {
if err := parseFlags(f); err != nil {
return RepoOptions{}, err
}
return RepoOptions{
func (f *RepoFlagGroup) ToOptions(opts *Options) error {
opts.RepoOptions = RepoOptions{
RepoBranch: f.Branch.Value(),
RepoCommit: f.Commit.Value(),
RepoTag: f.Tag.Value(),
}, nil
}
return nil
}

View File

@@ -200,11 +200,7 @@ func (f *ReportFlagGroup) Flags() []Flagger {
}
}
func (f *ReportFlagGroup) ToOptions() (ReportOptions, error) {
if err := parseFlags(f); err != nil {
return ReportOptions{}, err
}
func (f *ReportFlagGroup) ToOptions(opts *Options) error {
format := types.Format(f.Format.Value())
template := f.Template.Value()
dependencyTree := f.DependencyTree.Value()
@@ -241,27 +237,27 @@ func (f *ReportFlagGroup) ToOptions() (ReportOptions, error) {
// "--table-mode" option is available only with "--format table".
if viper.IsSet(TableModeFlag.ConfigName) && format != types.FormatTable {
return ReportOptions{}, xerrors.New(`"--table-mode" can be used only with "--format table".`)
return xerrors.New(`"--table-mode" can be used only with "--format table".`)
}
cs, err := loadComplianceTypes(f.Compliance.Value())
if err != nil {
return ReportOptions{}, xerrors.Errorf("unable to load compliance spec: %w", err)
return xerrors.Errorf("unable to load compliance spec: %w", err)
}
var outputPluginArgs []string
if arg := f.OutputPluginArg.Value(); arg != "" {
outputPluginArgs, err = shellwords.Parse(arg)
if err != nil {
return ReportOptions{}, xerrors.Errorf("unable to parse output plugin argument: %w", err)
return xerrors.Errorf("unable to parse output plugin argument: %w", err)
}
}
if viper.IsSet(f.IgnoreFile.ConfigName) && !fsutils.FileExists(f.IgnoreFile.Value()) {
return ReportOptions{}, xerrors.Errorf("ignore file not found: %s", f.IgnoreFile.Value())
return xerrors.Errorf("ignore file not found: %s", f.IgnoreFile.Value())
}
return ReportOptions{
opts.ReportOptions = ReportOptions{
Format: format,
ReportFormat: f.ReportFormat.Value(),
Template: template,
@@ -277,7 +273,8 @@ func (f *ReportFlagGroup) ToOptions() (ReportOptions, error) {
Compliance: cs,
ShowSuppressed: f.ShowSuppressed.Value(),
TableModes: xstrings.ToTSlice[types.TableMode](tableModes),
}, nil
}
return nil
}
func loadComplianceTypes(compliance string) (spec.ComplianceSpec, error) {

View File

@@ -214,13 +214,14 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
TableMode: flag.TableModeFlag.Clone(),
}
got, err := f.ToOptions()
flags := flag.Flags{f}
got, err := flags.ToOptions(nil)
if tt.wantErr != "" {
require.Contains(t, err.Error(), tt.wantErr)
require.ErrorContains(t, err, tt.wantErr)
return
}
assert.EqualExportedValuesf(t, tt.want, got, "ToOptions()")
assert.EqualExportedValues(t, tt.want, got.ReportOptions)
// Assert log messages
assert.Equal(t, tt.wantLogs, out.Messages(), tt.name)
@@ -235,7 +236,8 @@ func TestReportFlagGroup_ToOptions(t *testing.T) {
IgnoreFile: flag.IgnoreFileFlag.Clone(),
}
_, err := f.ToOptions()
flags := flag.Flags{f}
_, err := flags.ToOptions(nil)
assert.ErrorContains(t, err, "ignore file not found: doesntexist")
})
}

View File

@@ -184,14 +184,10 @@ func (f *ScanFlagGroup) Flags() []Flagger {
}
}
func (f *ScanFlagGroup) ToOptions(args []string) (ScanOptions, error) {
if err := parseFlags(f); err != nil {
return ScanOptions{}, err
}
func (f *ScanFlagGroup) ToOptions(opts *Options) error {
var target string
if len(args) == 1 {
target = args[0]
if len(opts.args) == 1 {
target = opts.args[0]
}
parallel := f.Parallel.Value()
@@ -204,7 +200,7 @@ func (f *ScanFlagGroup) ToOptions(args []string) (ScanOptions, error) {
if f.DistroFlag != nil && f.DistroFlag.Value() != "" {
family, version, _ := strings.Cut(f.DistroFlag.Value(), "/")
if !slices.Contains(ftypes.OSTypes, ftypes.OSType(family)) {
return ScanOptions{}, xerrors.Errorf("unknown OS family: %s, must be %q", family, ftypes.OSTypes)
return xerrors.Errorf("unknown OS family: %s, must be %q", family, ftypes.OSTypes)
}
distro = ftypes.OS{
Family: ftypes.OSType(family),
@@ -212,7 +208,7 @@ func (f *ScanFlagGroup) ToOptions(args []string) (ScanOptions, error) {
}
}
return ScanOptions{
opts.ScanOptions = ScanOptions{
Target: target,
SkipDirs: f.SkipDirs.Value(),
SkipFiles: f.SkipFiles.Value(),
@@ -224,5 +220,6 @@ func (f *ScanFlagGroup) ToOptions(args []string) (ScanOptions, error) {
RekorURL: f.RekorURL.Value(),
DetectionPriority: ftypes.DetectionPriority(f.DetectionPriority.Value()),
Distro: distro,
}, nil
}
return nil
}

View File

@@ -147,9 +147,10 @@ func TestScanFlagGroup_ToOptions(t *testing.T) {
DistroFlag: flag.DistroFlag.Clone(),
}
got, err := f.ToOptions(tt.args)
flags := flag.Flags{f}
got, err := flags.ToOptions(tt.args)
tt.assertion(t, err)
assert.Equalf(t, tt.want, got, "ToOptions()")
assert.Equal(t, tt.want, got.ScanOptions)
})
}
}

View File

@@ -31,12 +31,9 @@ func (f *SecretFlagGroup) Flags() []Flagger {
return []Flagger{f.SecretConfig}
}
func (f *SecretFlagGroup) ToOptions() (SecretOptions, error) {
if err := parseFlags(f); err != nil {
return SecretOptions{}, err
}
return SecretOptions{
func (f *SecretFlagGroup) ToOptions(opts *Options) error {
opts.SecretOptions = SecretOptions{
SecretConfigPath: f.SecretConfig.Value(),
}, nil
}
return nil
}

View File

@@ -82,11 +82,7 @@ func (f *VulnerabilityFlagGroup) Flags() []Flagger {
}
}
func (f *VulnerabilityFlagGroup) ToOptions() (VulnerabilityOptions, error) {
if err := parseFlags(f); err != nil {
return VulnerabilityOptions{}, err
}
func (f *VulnerabilityFlagGroup) ToOptions(opts *Options) error {
// Just convert string to dbTypes.Status as the validated values are passed here.
ignoreStatuses := lo.Map(f.IgnoreStatus.Value(), func(s string, _ int) dbTypes.Status {
return dbTypes.NewStatus(s)
@@ -110,12 +106,13 @@ func (f *VulnerabilityFlagGroup) ToOptions() (VulnerabilityOptions, error) {
}
log.Debug("Ignore statuses", log.Any("statuses", ignoreStatuses))
return VulnerabilityOptions{
opts.VulnerabilityOptions = VulnerabilityOptions{
IgnoreStatuses: ignoreStatuses,
VEXSources: lo.Map(f.VEX.Value(), func(s string, _ int) vex.Source {
return vex.NewSource(s)
}),
SkipVEXRepoUpdate: f.SkipVEXRepoUpdate.Value(),
VulnSeveritySources: xstrings.ToTSlice[dbTypes.SourceID](f.VulnSeveritySource.Value()),
}, nil
}
return nil
}