From 486091956aa5e658ba1adeef352f0a484f1d431c Mon Sep 17 00:00:00 2001 From: nikpivkin Date: Thu, 8 Aug 2024 20:45:07 +0700 Subject: [PATCH] feat: support for custom schemas Signed-off-by: nikpivkin --- pkg/commands/artifact/run.go | 111 +++++++++----- .../analyzer/config/azurearm/azurearm.go | 4 +- .../config/cloudformation/cloudformation.go | 4 +- pkg/fanal/analyzer/config/config.go | 7 +- pkg/fanal/analyzer/config/config_test.go | 17 ++- .../analyzer/config/dockerfile/docker.go | 4 +- pkg/fanal/analyzer/config/helm/helm.go | 4 +- pkg/fanal/analyzer/config/json/json.go | 4 +- pkg/fanal/analyzer/config/k8s/k8s.go | 4 +- .../analyzer/config/terraform/terraform.go | 3 +- .../config/terraformplan/json/json.go | 4 +- .../config/terraformplan/snapshot/snapshot.go | 4 +- pkg/fanal/analyzer/config/yaml/yaml.go | 4 +- .../analyzer/imgconf/dockerfile/dockerfile.go | 3 +- pkg/fanal/types/const.go | 1 + pkg/flag/misconf_flags.go | 19 ++- pkg/iac/detection/detect.go | 48 +++++- pkg/iac/detection/detect_test.go | 138 ++++++++++++++++++ pkg/iac/rego/build.go | 10 +- pkg/iac/rego/embed.go | 2 +- pkg/iac/rego/load.go | 2 +- pkg/iac/rego/scanner.go | 6 + pkg/iac/scanners/azure/arm/scanner.go | 3 +- pkg/iac/scanners/cloudformation/scanner.go | 3 +- pkg/iac/scanners/dockerfile/scanner.go | 3 +- pkg/iac/scanners/helm/scanner.go | 3 +- pkg/iac/scanners/json/scanner.go | 3 +- pkg/iac/scanners/kubernetes/scanner.go | 3 +- pkg/iac/scanners/options/scanner.go | 7 + pkg/iac/scanners/terraform/scanner.go | 3 +- .../scanners/terraformplan/tfjson/scanner.go | 3 +- pkg/iac/scanners/toml/scanner.go | 3 +- pkg/iac/scanners/yaml/scanner.go | 3 +- pkg/misconf/scanner.go | 111 ++++++++------ pkg/misconf/scanner_test.go | 18 ++- 35 files changed, 429 insertions(+), 140 deletions(-) diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index 3cd1b9af74b5..4e10b506fe63 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -4,7 +4,10 @@ import ( "context" "errors" "fmt" + "io/fs" + "path/filepath" "slices" + "strings" "github.com/hashicorp/go-multierror" "github.com/samber/lo" @@ -501,43 +504,13 @@ func (r *runner) initScannerConfig(opts flag.Options) (ScannerConfig, types.Scan log.WithPrefix(log.PrefixVulnerability).Info("Vulnerability scanning is enabled") } - // ScannerOption is filled only when config scanning is enabled. + // Misconfig ScannerOption is filled only when config scanning is enabled. var configScannerOptions misconf.ScannerOption if opts.Scanners.Enabled(types.MisconfigScanner) || opts.ImageConfigScanners.Enabled(types.MisconfigScanner) { - logger := log.WithPrefix(log.PrefixMisconfiguration) - logger.Info("Misconfiguration scanning is enabled") - - var downloadedPolicyPaths []string - var disableEmbedded bool - - downloadedPolicyPaths, err := operation.InitBuiltinPolicies(context.Background(), opts.CacheDir, opts.Quiet, opts.SkipCheckUpdate, opts.MisconfOptions.ChecksBundleRepository, opts.RegistryOpts()) + var err error + configScannerOptions, err = initMisconfScannerOption(opts) if err != nil { - if !opts.SkipCheckUpdate { - logger.Error("Falling back to embedded checks", log.Err(err)) - } - } else { - logger.Debug("Policies successfully loaded from disk") - disableEmbedded = true - } - configScannerOptions = misconf.ScannerOption{ - Debug: opts.Debug, - Trace: opts.Trace, - Namespaces: append(opts.CheckNamespaces, rego.BuiltinNamespaces()...), - PolicyPaths: append(opts.CheckPaths, downloadedPolicyPaths...), - DataPaths: opts.DataPaths, - HelmValues: opts.HelmValues, - HelmValueFiles: opts.HelmValueFiles, - HelmFileValues: opts.HelmFileValues, - HelmStringValues: opts.HelmStringValues, - HelmAPIVersions: opts.HelmAPIVersions, - HelmKubeVersion: opts.HelmKubeVersion, - TerraformTFVars: opts.TerraformTFVars, - CloudFormationParamVars: opts.CloudFormationParamVars, - K8sVersion: opts.K8sVersion, - DisableEmbeddedPolicies: disableEmbedded, - DisableEmbeddedLibraries: disableEmbedded, - IncludeDeprecatedChecks: opts.IncludeDeprecatedChecks, - TfExcludeDownloaded: opts.TfExcludeDownloaded, + return ScannerConfig{}, types.ScanOptions{}, err } } @@ -650,3 +623,73 @@ func (r *runner) scan(ctx context.Context, opts flag.Options, initializeScanner } return report, nil } + +func initMisconfScannerOption(opts flag.Options) (misconf.ScannerOption, error) { + logger := log.WithPrefix(log.PrefixMisconfiguration) + logger.Info("Misconfiguration scanning is enabled") + + var downloadedPolicyPaths []string + var disableEmbedded bool + + downloadedPolicyPaths, err := operation.InitBuiltinPolicies(context.Background(), opts.CacheDir, opts.Quiet, opts.SkipCheckUpdate, opts.MisconfOptions.ChecksBundleRepository, opts.RegistryOpts()) + if err != nil { + if !opts.SkipCheckUpdate { + logger.Error("Falling back to embedded checks", log.Err(err)) + } + } else { + logger.Debug("Policies successfully loaded from disk") + disableEmbedded = true + } + + configSchemas, err := loadConfigSchemas(opts.ConfigFileSchemas) + if err != nil { + return misconf.ScannerOption{}, xerrors.Errorf("load schemas error: %w", err) + } + + return misconf.ScannerOption{ + Debug: opts.Debug, + Trace: opts.Trace, + Namespaces: append(opts.CheckNamespaces, rego.BuiltinNamespaces()...), + PolicyPaths: append(opts.CheckPaths, downloadedPolicyPaths...), + DataPaths: opts.DataPaths, + HelmValues: opts.HelmValues, + HelmValueFiles: opts.HelmValueFiles, + HelmFileValues: opts.HelmFileValues, + HelmStringValues: opts.HelmStringValues, + HelmAPIVersions: opts.HelmAPIVersions, + HelmKubeVersion: opts.HelmKubeVersion, + TerraformTFVars: opts.TerraformTFVars, + CloudFormationParamVars: opts.CloudFormationParamVars, + K8sVersion: opts.K8sVersion, + DisableEmbeddedPolicies: disableEmbedded, + DisableEmbeddedLibraries: disableEmbedded, + IncludeDeprecatedChecks: opts.IncludeDeprecatedChecks, + TfExcludeDownloaded: opts.TfExcludeDownloaded, + FilePatterns: opts.FilePatterns, + ConfigFileSchemas: configSchemas, + }, nil +} + +func loadConfigSchemas(paths []string) ([]*misconf.ConfigFileSchema, error) { + var configSchemas []*misconf.ConfigFileSchema + for _, path := range paths { + filepath.WalkDir(path, func(path string, info fs.DirEntry, err error) error { + if err != nil { + return err + } + if info.IsDir() || !strings.HasSuffix(info.Name(), ".json") { + return nil + } + + schema, err := misconf.NewConfigFileSchema(path) + if err != nil { + return xerrors.Errorf("load config file schema: %w", err) + } + + configSchemas = append(configSchemas, schema) + return nil + }) + } + + return configSchemas, nil +} diff --git a/pkg/fanal/analyzer/config/azurearm/azurearm.go b/pkg/fanal/analyzer/config/azurearm/azurearm.go index 3c0c4b9828f1..ecd7826173a0 100644 --- a/pkg/fanal/analyzer/config/azurearm/azurearm.go +++ b/pkg/fanal/analyzer/config/azurearm/azurearm.go @@ -6,7 +6,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" - "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/iac/detection" ) const ( @@ -25,7 +25,7 @@ type azureARMConfigAnalyzer struct { } func newAzureARMConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewAzureARMScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeAzureARM, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/config/cloudformation/cloudformation.go b/pkg/fanal/analyzer/config/cloudformation/cloudformation.go index 06f7e458a859..02f281cbbb8a 100644 --- a/pkg/fanal/analyzer/config/cloudformation/cloudformation.go +++ b/pkg/fanal/analyzer/config/cloudformation/cloudformation.go @@ -3,7 +3,7 @@ package cloudformation import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" - "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/iac/detection" ) const ( @@ -22,7 +22,7 @@ type cloudFormationConfigAnalyzer struct { } func newCloudFormationConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewCloudFormationScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeCloudFormation, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/config/config.go b/pkg/fanal/analyzer/config/config.go index b5f3569d0ab4..020ae1ae3562 100644 --- a/pkg/fanal/analyzer/config/config.go +++ b/pkg/fanal/analyzer/config/config.go @@ -9,6 +9,7 @@ import ( "k8s.io/utils/strings/slices" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/iac/detection" "github.com/aquasecurity/trivy/pkg/misconf" ) @@ -26,10 +27,8 @@ type Analyzer struct { scanner *misconf.Scanner } -type NewScanner func([]string, misconf.ScannerOption) (*misconf.Scanner, error) - -func NewAnalyzer(t analyzer.Type, version int, newScanner NewScanner, opts analyzer.AnalyzerOptions) (*Analyzer, error) { - s, err := newScanner(opts.FilePatterns, opts.MisconfScannerOption) +func NewAnalyzer(t analyzer.Type, version int, fileType detection.FileType, opts analyzer.AnalyzerOptions) (*Analyzer, error) { + s, err := misconf.NewScanner(fileType, opts.MisconfScannerOption) if err != nil { return nil, xerrors.Errorf("%s scanner init error: %w", t, err) } diff --git a/pkg/fanal/analyzer/config/config_test.go b/pkg/fanal/analyzer/config/config_test.go index 147b1f4d3201..34503bde2995 100644 --- a/pkg/fanal/analyzer/config/config_test.go +++ b/pkg/fanal/analyzer/config/config_test.go @@ -12,14 +12,15 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/detection" "github.com/aquasecurity/trivy/pkg/misconf" ) func TestAnalyzer_PostAnalyze(t *testing.T) { type fields struct { - typ analyzer.Type - newScanner config.NewScanner - opts analyzer.AnalyzerOptions + typ analyzer.Type + fileType detection.FileType + opts analyzer.AnalyzerOptions } tests := []struct { name string @@ -31,8 +32,8 @@ func TestAnalyzer_PostAnalyze(t *testing.T) { { name: "dockerfile", fields: fields{ - typ: analyzer.TypeDockerfile, - newScanner: misconf.NewDockerfileScanner, + typ: analyzer.TypeDockerfile, + fileType: detection.FileTypeDockerfile, opts: analyzer.AnalyzerOptions{ MisconfScannerOption: misconf.ScannerOption{ Namespaces: []string{"user"}, @@ -74,8 +75,8 @@ func TestAnalyzer_PostAnalyze(t *testing.T) { { name: "non-existent dir", fields: fields{ - typ: analyzer.TypeDockerfile, - newScanner: misconf.NewDockerfileScanner, + typ: analyzer.TypeDockerfile, + fileType: detection.FileTypeDockerfile, opts: analyzer.AnalyzerOptions{ MisconfScannerOption: misconf.ScannerOption{ Namespaces: []string{"user"}, @@ -90,7 +91,7 @@ func TestAnalyzer_PostAnalyze(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - a, err := config.NewAnalyzer(tt.fields.typ, 0, tt.fields.newScanner, tt.fields.opts) + a, err := config.NewAnalyzer(tt.fields.typ, 0, tt.fields.fileType, tt.fields.opts) require.NoError(t, err) got, err := a.PostAnalyze(context.Background(), analyzer.PostAnalysisInput{ diff --git a/pkg/fanal/analyzer/config/dockerfile/docker.go b/pkg/fanal/analyzer/config/dockerfile/docker.go index 353cef4eb62a..0c4463dd528e 100644 --- a/pkg/fanal/analyzer/config/dockerfile/docker.go +++ b/pkg/fanal/analyzer/config/dockerfile/docker.go @@ -7,7 +7,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" - "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/iac/detection" ) const ( @@ -28,7 +28,7 @@ type dockerConfigAnalyzer struct { } func newDockerfileConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewDockerfileScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeDockerfile, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/config/helm/helm.go b/pkg/fanal/analyzer/config/helm/helm.go index 14ea6aff63de..42542ce6c835 100644 --- a/pkg/fanal/analyzer/config/helm/helm.go +++ b/pkg/fanal/analyzer/config/helm/helm.go @@ -7,7 +7,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" - "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/iac/detection" ) const ( @@ -29,7 +29,7 @@ type helmConfigAnalyzer struct { } func newHelmConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewHelmScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeHelm, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/config/json/json.go b/pkg/fanal/analyzer/config/json/json.go index 1d1a70169996..c2a6eaa73d63 100644 --- a/pkg/fanal/analyzer/config/json/json.go +++ b/pkg/fanal/analyzer/config/json/json.go @@ -6,7 +6,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" - "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/iac/detection" ) const ( @@ -24,7 +24,7 @@ type jsonConfigAnalyzer struct { } func newJSONConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewJSONScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeJSON, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/config/k8s/k8s.go b/pkg/fanal/analyzer/config/k8s/k8s.go index 6f3a58af16b5..dabdf41a990b 100644 --- a/pkg/fanal/analyzer/config/k8s/k8s.go +++ b/pkg/fanal/analyzer/config/k8s/k8s.go @@ -3,7 +3,7 @@ package k8s import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" - "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/iac/detection" ) const ( @@ -22,7 +22,7 @@ type kubernetesConfigAnalyzer struct { } func newKubernetesConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewKubernetesScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeKubernetes, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/config/terraform/terraform.go b/pkg/fanal/analyzer/config/terraform/terraform.go index 363d35de87fe..14b683742413 100644 --- a/pkg/fanal/analyzer/config/terraform/terraform.go +++ b/pkg/fanal/analyzer/config/terraform/terraform.go @@ -6,7 +6,6 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" "github.com/aquasecurity/trivy/pkg/iac/detection" - "github.com/aquasecurity/trivy/pkg/misconf" ) const ( @@ -25,7 +24,7 @@ type terraformConfigAnalyzer struct { } func newTerraformConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewTerraformScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeTerraform, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/config/terraformplan/json/json.go b/pkg/fanal/analyzer/config/terraformplan/json/json.go index 5272f0f990f9..f0cb2c518549 100644 --- a/pkg/fanal/analyzer/config/terraformplan/json/json.go +++ b/pkg/fanal/analyzer/config/terraformplan/json/json.go @@ -8,7 +8,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" - "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/iac/detection" ) const ( @@ -31,7 +31,7 @@ type terraformPlanConfigAnalyzer struct { } func newTerraformPlanJSONConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewTerraformPlanJSONScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeTerraformPlanJSON, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot.go b/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot.go index 0597c137d96c..13316914874d 100644 --- a/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot.go +++ b/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot.go @@ -6,7 +6,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" - "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/iac/detection" ) const ( @@ -25,7 +25,7 @@ type terraformPlanConfigAnalyzer struct { } func newTerraformPlanSnapshotConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewTerraformPlanSnapshotScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeTerraformPlanSnapshot, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/config/yaml/yaml.go b/pkg/fanal/analyzer/config/yaml/yaml.go index a27ef20d4a39..f8b5569f5bed 100644 --- a/pkg/fanal/analyzer/config/yaml/yaml.go +++ b/pkg/fanal/analyzer/config/yaml/yaml.go @@ -6,7 +6,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" - "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/iac/detection" ) const ( @@ -24,7 +24,7 @@ type yamlConfigAnalyzer struct { } func newYAMLConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { - a, err := config.NewAnalyzer(analyzerType, version, misconf.NewYAMLScanner, opts) + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeYAML, opts) if err != nil { return nil, err } diff --git a/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile.go b/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile.go index e0c70cc34cd0..5a974a8dd1d1 100644 --- a/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile.go +++ b/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile.go @@ -11,6 +11,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/image" "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/detection" "github.com/aquasecurity/trivy/pkg/mapfs" "github.com/aquasecurity/trivy/pkg/misconf" ) @@ -26,7 +27,7 @@ type historyAnalyzer struct { } func newHistoryAnalyzer(opts analyzer.ConfigAnalyzerOptions) (analyzer.ConfigAnalyzer, error) { - s, err := misconf.NewDockerfileScanner(opts.FilePatterns, opts.MisconfScannerOption) + s, err := misconf.NewScanner(detection.FileTypeDockerfile, opts.MisconfScannerOption) if err != nil { return nil, xerrors.Errorf("misconfiguration scanner error: %w", err) } diff --git a/pkg/fanal/types/const.go b/pkg/fanal/types/const.go index c257154e24ea..ffe1e0718764 100644 --- a/pkg/fanal/types/const.go +++ b/pkg/fanal/types/const.go @@ -97,6 +97,7 @@ var AggregatingTypes = []LangType{ // Config files const ( JSON ConfigType = "json" + YAML ConfigType = "yaml" Dockerfile ConfigType = "dockerfile" Terraform ConfigType = "terraform" TerraformPlanJSON ConfigType = "terraformplan" diff --git a/pkg/flag/misconf_flags.go b/pkg/flag/misconf_flags.go index fc7505fec393..47631d0b5f88 100644 --- a/pkg/flag/misconf_flags.go +++ b/pkg/flag/misconf_flags.go @@ -3,6 +3,8 @@ package flag import ( "fmt" + "github.com/samber/lo" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/policy" xstrings "github.com/aquasecurity/trivy/pkg/x/strings" @@ -96,8 +98,16 @@ var ( MisconfigScannersFlag = Flag[[]string]{ Name: "misconfig-scanners", ConfigName: "misconfiguration.scanners", - Default: xstrings.ToStringSlice(analyzer.TypeConfigFiles), - Usage: "comma-separated list of misconfig scanners to use for misconfiguration scanning", + Default: xstrings.ToStringSlice( + lo.Without(analyzer.TypeConfigFiles, analyzer.TypeYAML, analyzer.TypeJSON), + ), + Usage: "comma-separated list of misconfig scanners to use for misconfiguration scanning", + } + ConfigFileSchemasFlag = Flag[[]string]{ + Name: "config-file-schemas", + ConfigName: "misconfiguration.config-file-schemas", + // TODO(nikpivkin): rewrite usage + Usage: "specify paths to configuration file schemas to determine that a file matches some configuration and pass to Rego checks for type checking", } ) @@ -118,6 +128,7 @@ type MisconfFlagGroup struct { CloudformationParamVars *Flag[[]string] TerraformExcludeDownloaded *Flag[bool] MisconfigScanners *Flag[[]string] + ConfigFileSchemas *Flag[[]string] } type MisconfOptions struct { @@ -136,6 +147,7 @@ type MisconfOptions struct { CloudFormationParamVars []string TfExcludeDownloaded bool MisconfigScanners []analyzer.Type + ConfigFileSchemas []string } func NewMisconfFlagGroup() *MisconfFlagGroup { @@ -154,6 +166,7 @@ func NewMisconfFlagGroup() *MisconfFlagGroup { CloudformationParamVars: CfParamsFlag.Clone(), TerraformExcludeDownloaded: TerraformExcludeDownloaded.Clone(), MisconfigScanners: MisconfigScannersFlag.Clone(), + ConfigFileSchemas: ConfigFileSchemasFlag.Clone(), } } @@ -176,6 +189,7 @@ func (f *MisconfFlagGroup) Flags() []Flagger { f.TerraformExcludeDownloaded, f.CloudformationParamVars, f.MisconfigScanners, + f.ConfigFileSchemas, } } @@ -198,5 +212,6 @@ func (f *MisconfFlagGroup) ToOptions() (MisconfOptions, error) { CloudFormationParamVars: f.CloudformationParamVars.Value(), TfExcludeDownloaded: f.TerraformExcludeDownloaded.Value(), MisconfigScanners: xstrings.ToTSlice[analyzer.Type](f.MisconfigScanners.Value()), + ConfigFileSchemas: f.ConfigFileSchemas.Value(), }, nil } diff --git a/pkg/iac/detection/detect.go b/pkg/iac/detection/detect.go index cec90a79cdca..92cbfc38f7b3 100644 --- a/pkg/iac/detection/detect.go +++ b/pkg/iac/detection/detect.go @@ -7,11 +7,13 @@ import ( "path/filepath" "strings" + "github.com/xeipuuv/gojsonschema" "gopkg.in/yaml.v3" "github.com/aquasecurity/trivy/pkg/iac/scanners/azure/arm/parser/armjson" "github.com/aquasecurity/trivy/pkg/iac/scanners/terraformplan/snapshot" "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" ) type FileType string @@ -33,12 +35,12 @@ const ( var matchers = make(map[FileType]func(name string, r io.ReadSeeker) bool) +// TODO(nikita): refactor. If the file matches the schema, it no longer needs to be checked for other scanners. // nolint func init() { matchers[FileTypeJSON] = func(name string, r io.ReadSeeker) bool { - ext := filepath.Ext(filepath.Base(name)) - if !strings.EqualFold(ext, ".json") { + if !isJSON(name) { return false } if resetReader(r) == nil { @@ -50,8 +52,7 @@ func init() { } matchers[FileTypeYAML] = func(name string, r io.ReadSeeker) bool { - ext := filepath.Ext(filepath.Base(name)) - if !strings.EqualFold(ext, ".yaml") && !strings.EqualFold(ext, ".yml") { + if !isYAML(name) { return false } if resetReader(r) == nil { @@ -309,3 +310,42 @@ func resetReader(r io.Reader) io.ReadSeeker { } return ensureSeeker(r) } + +func isJSON(name string) bool { + ext := filepath.Ext(name) + return strings.EqualFold(ext, ".json") +} +func isYAML(name string) bool { + ext := filepath.Ext(name) + return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") +} + +func IsSchemaMatch(schemas map[string]*gojsonschema.Schema, typ FileType, name string, r io.ReadSeeker) bool { + defer resetReader(r) + + var l gojsonschema.JSONLoader + switch { + case typ == FileTypeJSON && isJSON(name): + b, err := io.ReadAll(r) + if err != nil { + return false + } + l = gojsonschema.NewBytesLoader(b) + case typ == FileTypeYAML && isYAML(name): + var content any + if err := yaml.NewDecoder(r).Decode(&content); err != nil { + return false + } + l = gojsonschema.NewGoLoader(content) + default: + return false + } + + for schemaPath, schema := range schemas { + if res, err := schema.Validate(l); err == nil && res.Valid() { + log.Debug("The file matches the schema", log.FilePath(name), log.String("schema_path", schemaPath)) + return true + } + } + return false +} diff --git a/pkg/iac/detection/detect_test.go b/pkg/iac/detection/detect_test.go index 20998427d99d..cd5af058be6e 100644 --- a/pkg/iac/detection/detect_test.go +++ b/pkg/iac/detection/detect_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/xeipuuv/gojsonschema" ) func Test_Detection(t *testing.T) { @@ -473,3 +474,140 @@ func BenchmarkIsType_BigFile(b *testing.B) { _ = IsType(fmt.Sprintf("./testdata/%s", "big.file"), bytes.NewReader(data), FileTypeAzureARM) } } + +func Test_IsSchemaMatch(t *testing.T) { + + schema := `{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "service": { "type": "string" } + }, + "required": ["service"] +}` + + schema2 := `{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "provider": { "type": "string" } + }, + "required": ["provider"] + }` + + type args struct { + schemas []string + fileType FileType + fileName string + fileContent string + } + tests := []struct { + name string + args args + matches bool + }{ + { + name: "json file matches", + args: args{ + schemas: []string{schema}, + fileType: FileTypeJSON, + fileName: "test.json", + fileContent: `{ + "service": "test" +}`, + }, + matches: true, + }, + { + name: "json file dost not matches", + args: args{ + schemas: []string{schema}, + fileType: FileTypeJSON, + fileName: "test.json", + fileContent: `{ + "somefield": "test", +}`, + }, + matches: false, + }, + { + name: "json file matches, but file type is yaml", + args: args{ + schemas: []string{schema}, + fileType: FileTypeYAML, + fileName: "test.json", + fileContent: `{ + "service": "test" +}`, + }, + matches: false, + }, + { + name: "broken json file", + args: args{ + schemas: []string{schema}, + fileType: FileTypeJSON, + fileName: "test.json", + fileContent: `{ + "service": "test",, +}`, + }, + matches: false, + }, + { + name: "yaml file matches", + args: args{ + schemas: []string{schema}, + fileType: FileTypeYAML, + fileName: "test.yml", + fileContent: `service: test`, + }, + matches: true, + }, + { + name: "yaml file dost not matches", + args: args{ + schemas: []string{schema}, + fileType: FileTypeYAML, + fileName: "test.yaml", + fileContent: `somefield: test`, + }, + matches: false, + }, + { + name: "broken yaml file", + args: args{ + schemas: []string{schema}, + fileType: FileTypeYAML, + fileName: "test.yaml", + fileContent: `text foobar +number: 2`, + }, + matches: false, + }, + { + name: "multiple schemas", + args: args{ + schemas: []string{schema, schema2}, + fileType: FileTypeYAML, + fileName: "test.yaml", + fileContent: `provider: test`, + }, + matches: true, + }, + } + for _, tt := range tests { + schemas := make(map[string]*gojsonschema.Schema) + for i, content := range tt.args.schemas { + l := gojsonschema.NewStringLoader(content) + s, err := gojsonschema.NewSchema(l) + require.NoError(t, err) + schemas[fmt.Sprintf("shema-%d.json", i)] = s + } + rs := strings.NewReader(tt.args.fileContent) + got := IsSchemaMatch(schemas, tt.args.fileType, tt.args.fileName, rs) + assert.Equal(t, tt.matches, got) + } +} diff --git a/pkg/iac/rego/build.go b/pkg/iac/rego/build.go index a56ee042fb52..6c84c52617fb 100644 --- a/pkg/iac/rego/build.go +++ b/pkg/iac/rego/build.go @@ -13,7 +13,7 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/types" ) -func BuildSchemaSetFromPolicies(policies map[string]*ast.Module, paths []string, fsys fs.FS) (*ast.SchemaSet, bool, error) { +func BuildSchemaSetFromPolicies(policies map[string]*ast.Module, paths []string, fsys fs.FS, customSchemas map[string][]byte) (*ast.SchemaSet, bool, error) { schemaSet := ast.NewSchemaSet() schemaSet.Put(ast.MustParseRef("schema.input"), make(map[string]any)) // for backwards compat only var customFound bool @@ -26,9 +26,15 @@ func BuildSchemaSetFromPolicies(policies map[string]*ast.Module, paths []string, continue } + if schemaSet.Get(ss.Schema) != nil { + continue + } + var schema []byte if s, ok := schemas.SchemaMap[types.Source(schemaName)]; ok { schema = []byte(s) + } else if s, ok := customSchemas[schemaName]; ok { + schema = s } else { b, err := findSchemaInFS(paths, fsys, schemaName) if err != nil { @@ -47,7 +53,7 @@ func BuildSchemaSetFromPolicies(policies map[string]*ast.Module, paths []string, return schemaSet, false, fmt.Errorf("could not parse schema %q: %w", schemaName, err) } customFound = true - schemaSet.Put(ast.MustParseRef(ss.Schema.String()), rawSchema) + schemaSet.Put(ss.Schema, rawSchema) } } } diff --git a/pkg/iac/rego/embed.go b/pkg/iac/rego/embed.go index 9d1ac6458c52..6f542d9a0b2b 100644 --- a/pkg/iac/rego/embed.go +++ b/pkg/iac/rego/embed.go @@ -33,7 +33,7 @@ func init() { func RegisterRegoRules(modules map[string]*ast.Module) { ctx := context.TODO() - schemaSet, _, _ := BuildSchemaSetFromPolicies(modules, nil, nil) + schemaSet, _, _ := BuildSchemaSetFromPolicies(modules, nil, nil, make(map[string][]byte)) compiler := ast.NewCompiler(). WithSchemas(schemaSet). diff --git a/pkg/iac/rego/load.go b/pkg/iac/rego/load.go index f2e4c0645c4f..bd474a23be4d 100644 --- a/pkg/iac/rego/load.go +++ b/pkg/iac/rego/load.go @@ -230,7 +230,7 @@ func (s *Scanner) prunePoliciesWithError(compiler *ast.Compiler) error { func (s *Scanner) compilePolicies(srcFS fs.FS, paths []string) error { - schemaSet, custom, err := BuildSchemaSetFromPolicies(s.policies, paths, srcFS) + schemaSet, custom, err := BuildSchemaSetFromPolicies(s.policies, paths, srcFS, s.customSchemas) if err != nil { return err } diff --git a/pkg/iac/rego/scanner.go b/pkg/iac/rego/scanner.go index f7293c46a0c5..fe0553d6bc00 100644 --- a/pkg/iac/rego/scanner.go +++ b/pkg/iac/rego/scanner.go @@ -65,6 +65,7 @@ type Scanner struct { embeddedLibs map[string]*ast.Module embeddedChecks map[string]*ast.Module + customSchemas map[string][]byte } func (s *Scanner) SetIncludeDeprecatedChecks(b bool) { @@ -142,6 +143,10 @@ func (s *Scanner) SetRegoErrorLimit(limit int) { s.regoErrorLimit = limit } +func (s *Scanner) SetCustomSchemas(v map[string][]byte) { + s.customSchemas = v +} + type DynamicMetadata struct { Warning bool Filepath string @@ -161,6 +166,7 @@ func NewScanner(source types.Source, opts ...options.ScannerOption) *Scanner { sourceType: source, ruleNamespaces: make(map[string]struct{}), runtimeValues: addRuntimeValues(), + customSchemas: make(map[string][]byte), } maps.Copy(s.ruleNamespaces, builtinNamespaces) diff --git a/pkg/iac/scanners/azure/arm/scanner.go b/pkg/iac/scanners/azure/arm/scanner.go index 871b58df3f3d..fd585a5955c5 100644 --- a/pkg/iac/scanners/azure/arm/scanner.go +++ b/pkg/iac/scanners/azure/arm/scanner.go @@ -39,7 +39,8 @@ type Scanner struct { spec string } -func (s *Scanner) SetIncludeDeprecatedChecks(b bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(b bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) SetSpec(spec string) { s.spec = spec diff --git a/pkg/iac/scanners/cloudformation/scanner.go b/pkg/iac/scanners/cloudformation/scanner.go index 20b96ce947fd..cedd18ea6297 100644 --- a/pkg/iac/scanners/cloudformation/scanner.go +++ b/pkg/iac/scanners/cloudformation/scanner.go @@ -63,7 +63,8 @@ type Scanner struct { spec string } -func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) addParserOptions(opt options.ParserOption) { s.parserOptions = append(s.parserOptions, opt) diff --git a/pkg/iac/scanners/dockerfile/scanner.go b/pkg/iac/scanners/dockerfile/scanner.go index 561872c70636..6358aada12c2 100644 --- a/pkg/iac/scanners/dockerfile/scanner.go +++ b/pkg/iac/scanners/dockerfile/scanner.go @@ -34,7 +34,8 @@ type Scanner struct { loadEmbeddedPolicies bool } -func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) SetSpec(spec string) { s.spec = spec diff --git a/pkg/iac/scanners/helm/scanner.go b/pkg/iac/scanners/helm/scanner.go index fe74911c51bc..944187df4dee 100644 --- a/pkg/iac/scanners/helm/scanner.go +++ b/pkg/iac/scanners/helm/scanner.go @@ -42,7 +42,8 @@ type Scanner struct { mu sync.Mutex } -func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) SetSpec(spec string) { s.spec = spec diff --git a/pkg/iac/scanners/json/scanner.go b/pkg/iac/scanners/json/scanner.go index 3aa0dffdb485..7a18df363823 100644 --- a/pkg/iac/scanners/json/scanner.go +++ b/pkg/iac/scanners/json/scanner.go @@ -34,7 +34,8 @@ type Scanner struct { loadEmbeddedLibraries bool } -func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) SetRegoOnly(bool) { } diff --git a/pkg/iac/scanners/kubernetes/scanner.go b/pkg/iac/scanners/kubernetes/scanner.go index c5437f292d85..9612fe03ebe4 100644 --- a/pkg/iac/scanners/kubernetes/scanner.go +++ b/pkg/iac/scanners/kubernetes/scanner.go @@ -38,7 +38,8 @@ type Scanner struct { loadEmbeddedLibraries bool } -func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) SetSpec(spec string) { s.spec = spec diff --git a/pkg/iac/scanners/options/scanner.go b/pkg/iac/scanners/options/scanner.go index 291400887037..f5b3b982ee67 100644 --- a/pkg/iac/scanners/options/scanner.go +++ b/pkg/iac/scanners/options/scanner.go @@ -24,6 +24,7 @@ type ConfigurableScanner interface { SetRegoErrorLimit(limit int) SetUseEmbeddedLibraries(bool) SetIncludeDeprecatedChecks(bool) + SetCustomSchemas(map[string][]byte) } type ScannerOption func(s ConfigurableScanner) @@ -126,3 +127,9 @@ func ScannerWithRegoErrorLimits(limit int) ScannerOption { s.SetRegoErrorLimit(limit) } } + +func ScannerWithCustomSchemas(schemas map[string][]byte) ScannerOption { + return func(s ConfigurableScanner) { + s.SetCustomSchemas(schemas) + } +} diff --git a/pkg/iac/scanners/terraform/scanner.go b/pkg/iac/scanners/terraform/scanner.go index a64201e1fcc5..5e7cea83abfd 100644 --- a/pkg/iac/scanners/terraform/scanner.go +++ b/pkg/iac/scanners/terraform/scanner.go @@ -45,7 +45,8 @@ type Scanner struct { loadEmbeddedPolicies bool } -func (s *Scanner) SetIncludeDeprecatedChecks(b bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(b bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) SetSpec(spec string) { s.spec = spec diff --git a/pkg/iac/scanners/terraformplan/tfjson/scanner.go b/pkg/iac/scanners/terraformplan/tfjson/scanner.go index b25eed6ae42b..b390d4d10213 100644 --- a/pkg/iac/scanners/terraformplan/tfjson/scanner.go +++ b/pkg/iac/scanners/terraformplan/tfjson/scanner.go @@ -38,7 +38,8 @@ type Scanner struct { policyReaders []io.Reader } -func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) SetUseEmbeddedLibraries(b bool) { s.loadEmbeddedLibraries = b diff --git a/pkg/iac/scanners/toml/scanner.go b/pkg/iac/scanners/toml/scanner.go index 37e1807ac254..b7dc3510da8f 100644 --- a/pkg/iac/scanners/toml/scanner.go +++ b/pkg/iac/scanners/toml/scanner.go @@ -32,7 +32,8 @@ type Scanner struct { loadEmbeddedLibraries bool } -func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) SetRegoOnly(bool) {} diff --git a/pkg/iac/scanners/yaml/scanner.go b/pkg/iac/scanners/yaml/scanner.go index 534ccbd8cb7b..3ec508fdd6f5 100644 --- a/pkg/iac/scanners/yaml/scanner.go +++ b/pkg/iac/scanners/yaml/scanner.go @@ -32,7 +32,8 @@ type Scanner struct { loadEmbeddedPolicies bool } -func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetIncludeDeprecatedChecks(bool) {} +func (s *Scanner) SetCustomSchemas(map[string][]byte) {} func (s *Scanner) SetRegoOnly(bool) {} diff --git a/pkg/misconf/scanner.go b/pkg/misconf/scanner.go index a5197fee5b83..255a390b0b19 100644 --- a/pkg/misconf/scanner.go +++ b/pkg/misconf/scanner.go @@ -1,6 +1,7 @@ package misconf import ( + "bytes" "context" "errors" "fmt" @@ -8,10 +9,12 @@ import ( "io/fs" "os" "path/filepath" + "regexp" "sort" "strings" "github.com/samber/lo" + "github.com/xeipuuv/gojsonschema" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/fanal/types" @@ -45,6 +48,8 @@ var enablediacTypes = map[detection.FileType]types.ConfigType{ detection.FileTypeHelm: types.Helm, detection.FileTypeTerraformPlanJSON: types.TerraformPlanJSON, detection.FileTypeTerraformPlanSnapshot: types.TerraformPlanSnapshot, + detection.FileTypeJSON: types.JSON, + detection.FileTypeYAML: types.YAML, } type ScannerOption struct { @@ -68,61 +73,59 @@ type ScannerOption struct { CloudFormationParamVars []string TfExcludeDownloaded bool K8sVersion string -} - -func (o *ScannerOption) Sort() { - sort.Strings(o.Namespaces) - sort.Strings(o.PolicyPaths) - sort.Strings(o.DataPaths) -} - -type Scanner struct { - fileType detection.FileType - scanner scanners.FSScanner - hasFilePattern bool -} - -func NewAzureARMScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeAzureARM, filePatterns, opt) -} -func NewCloudFormationScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeCloudFormation, filePatterns, opt) + FilePatterns []string + ConfigFileSchemas []*ConfigFileSchema } -func NewDockerfileScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeDockerfile, filePatterns, opt) +type ConfigFileSchema struct { + path string + name string + source []byte + schema *gojsonschema.Schema } -func NewHelmScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeHelm, filePatterns, opt) -} +func NewConfigFileSchema(path string) (*ConfigFileSchema, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, xerrors.Errorf("read config schema error: %w", err) + } -func NewKubernetesScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeKubernetes, filePatterns, opt) -} + // Go's regular expression engine does not support \Z + b = bytes.ReplaceAll(b, []byte(`\\Z`), []byte(`$`)) -func NewTerraformScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeTerraform, filePatterns, opt) -} + // Go's regular expression engine does not support negative lookahead + b = regexp.MustCompile(`\(\?\!.*\)`).ReplaceAll(b, []byte{}) + schema, err := gojsonschema.NewSchema(gojsonschema.NewBytesLoader(b)) + if err != nil { + return nil, xerrors.Errorf("compile config schema %s error: %w", path, err) + } -func NewTerraformPlanJSONScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeTerraformPlanJSON, filePatterns, opt) -} + fileName := filepath.Base(path) + schemaName := strings.TrimSuffix(fileName, filepath.Ext(fileName)) -func NewTerraformPlanSnapshotScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeTerraformPlanSnapshot, filePatterns, opt) + return &ConfigFileSchema{ + path: path, + name: schemaName, + schema: schema, + source: b, + }, nil } -func NewYAMLScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeYAML, filePatterns, opt) +func (o *ScannerOption) Sort() { + sort.Strings(o.Namespaces) + sort.Strings(o.PolicyPaths) + sort.Strings(o.DataPaths) } -func NewJSONScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) { - return newScanner(detection.FileTypeJSON, filePatterns, opt) +type Scanner struct { + fileType detection.FileType + scanner scanners.FSScanner + hasFilePattern bool + configFileSchemas []*ConfigFileSchema } -func newScanner(t detection.FileType, filePatterns []string, opt ScannerOption) (*Scanner, error) { +func NewScanner(t detection.FileType, opt ScannerOption) (*Scanner, error) { opts, err := scannerOptions(t, opt) if err != nil { return nil, err @@ -155,9 +158,10 @@ func newScanner(t detection.FileType, filePatterns []string, opt ScannerOption) } return &Scanner{ - fileType: t, - scanner: scanner, - hasFilePattern: hasFilePattern(t, filePatterns), + fileType: t, + scanner: scanner, + hasFilePattern: hasFilePattern(t, opt.FilePatterns), + configFileSchemas: opt.ConfigFileSchemas, }, nil } @@ -207,15 +211,26 @@ func (s *Scanner) filterFS(fsys fs.FS) (fs.FS, error) { if err != nil { return false, err } + defer file.Close() + rs, ok := file.(io.ReadSeeker) if !ok { return false, xerrors.Errorf("type assertion error: %w", err) } - defer file.Close() - if !s.hasFilePattern && !detection.IsType(path, rs, s.fileType) { + // TODO: to avoid cyclic import + schemas := lo.SliceToMap(s.configFileSchemas, func(schema *ConfigFileSchema) (string, *gojsonschema.Schema) { + return schema.path, schema.schema + }) + + if len(s.configFileSchemas) > 0 && + (s.fileType == detection.FileTypeYAML || s.fileType == detection.FileTypeJSON) && + !detection.IsSchemaMatch(schemas, s.fileType, path, rs) { + return true, nil + } else if !s.hasFilePattern && !detection.IsType(path, rs, s.fileType) { return true, nil } + foundRelevantFile = true return false, nil } @@ -248,9 +263,15 @@ func scannerOptions(t detection.FileType, opt ScannerOption) ([]options.ScannerO if err != nil { return nil, err } + + schemas := lo.SliceToMap(opt.ConfigFileSchemas, func(schema *ConfigFileSchema) (string, []byte) { + return schema.name, schema.source + }) + opts = append(opts, options.ScannerWithDataDirs(dataPaths...), options.ScannerWithDataFilesystem(dataFS), + options.ScannerWithCustomSchemas(schemas), ) if opt.Debug { diff --git a/pkg/misconf/scanner_test.go b/pkg/misconf/scanner_test.go index 6270b78b65e2..c545efee3b63 100644 --- a/pkg/misconf/scanner_test.go +++ b/pkg/misconf/scanner_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/detection" "github.com/aquasecurity/trivy/pkg/mapfs" ) @@ -91,7 +92,7 @@ func TestScanner_Scan(t *testing.T) { } tests := []struct { name string - scannerFunc func(filePatterns []string, opt ScannerOption) (*Scanner, error) + fileType detection.FileType fields fields files []file wantFilePath string @@ -99,8 +100,8 @@ func TestScanner_Scan(t *testing.T) { misconfsExpected int }{ { - name: "happy path. Dockerfile", - scannerFunc: NewDockerfileScanner, + name: "happy path. Dockerfile", + fileType: detection.FileTypeDockerfile, fields: fields{ opt: ScannerOption{}, }, @@ -115,8 +116,8 @@ func TestScanner_Scan(t *testing.T) { misconfsExpected: 1, }, { - name: "happy path. Dockerfile with custom file name", - scannerFunc: NewDockerfileScanner, + name: "happy path. Dockerfile with custom file name", + fileType: detection.FileTypeDockerfile, fields: fields{ filePatterns: []string{"dockerfile:dockerf"}, opt: ScannerOption{}, @@ -132,8 +133,8 @@ func TestScanner_Scan(t *testing.T) { misconfsExpected: 1, }, { - name: "happy path. terraform plan file", - scannerFunc: NewTerraformPlanJSONScanner, + name: "happy path. terraform plan file", + fileType: detection.FileTypeTerraformPlanJSON, files: []file{ { path: "main.tfplan.json", @@ -154,7 +155,8 @@ func TestScanner_Scan(t *testing.T) { require.NoError(t, err) } - s, err := tt.scannerFunc(tt.fields.filePatterns, tt.fields.opt) + // s, err := tt.scannerFunc(tt.fields.filePatterns, tt.fields.opt) + s, err := NewScanner(tt.fileType, tt.fields.opt) require.NoError(t, err) misconfs, err := s.Scan(context.Background(), fsys)