From 5f4b15aa7498b6369e1519ab889c61b11b5c8643 Mon Sep 17 00:00:00 2001 From: Pankaj Patil Date: Thu, 3 Jun 2021 19:49:57 +0530 Subject: [PATCH] Adds support to scan config resources with applicable policies & Refactors filteration (#803) * implement regometada filters * tests * Policy type filter added (#29) * policy type filter added * variable name issue fixed 1. cloud type --> policytypes * more unit tests * fix validation * adds header to new files * incorporate review comments Co-authored-by: Gaurav Gogia <16029099+gaurav-gogia@users.noreply.github.com> --- pkg/filters/filter-specs.go | 131 +++++++++++ pkg/filters/filters.go | 88 ++++++++ pkg/filters/filters_test.go | 358 ++++++++++++++++++++++++++++++ pkg/policy/interface.go | 24 +- pkg/policy/opa/engine.go | 143 +++--------- pkg/policy/opa/engine_test.go | 398 ---------------------------------- pkg/policy/opa/types.go | 23 +- pkg/policy/types.go | 22 ++ pkg/runtime/executor.go | 14 +- pkg/runtime/executor_test.go | 206 +++++++++--------- pkg/runtime/validate.go | 12 +- pkg/runtime/validate_test.go | 104 ++++----- pkg/utils/policy.go | 34 +++ 13 files changed, 856 insertions(+), 701 deletions(-) create mode 100644 pkg/filters/filter-specs.go create mode 100644 pkg/filters/filters.go create mode 100644 pkg/filters/filters_test.go delete mode 100644 pkg/policy/opa/engine_test.go diff --git a/pkg/filters/filter-specs.go b/pkg/filters/filter-specs.go new file mode 100644 index 000000000..4fbda084c --- /dev/null +++ b/pkg/filters/filter-specs.go @@ -0,0 +1,131 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package filters + +import ( + "github.com/accurics/terrascan/pkg/policy" + "github.com/accurics/terrascan/pkg/utils" +) + +// PolicyTypesFilterSpecification is policy type based Filter Spec +type PolicyTypesFilterSpecification struct { + policyTypes []string +} + +// IsSatisfied implementation for policy type based Filter spec +func (p PolicyTypesFilterSpecification) IsSatisfied(r *policy.RegoMetadata) bool { + // if policy type is not present for rego metadata, + // or if policy types is not specified, return true + if len(r.PolicyType) < 1 || len(p.policyTypes) < 1 { + return true + } + return utils.CheckPolicyType(r.PolicyType, p.policyTypes) +} + +// ResourceTypeFilterSpecification is resource type based Filter Spec +type ResourceTypeFilterSpecification struct { + resourceType string +} + +// IsSatisfied implementation for resource type based Filter spec +func (rs ResourceTypeFilterSpecification) IsSatisfied(r *policy.RegoMetadata) bool { + // if resource type is not present for rego metadata, return true + if len(r.ResourceType) < 1 { + return true + } + return rs.resourceType == r.ResourceType +} + +// RerefenceIDFilterSpecification is reference ID based Filter Spec +type RerefenceIDFilterSpecification struct { + ReferenceID string +} + +// IsSatisfied implementation for reference ID based Filter spec +func (rs RerefenceIDFilterSpecification) IsSatisfied(r *policy.RegoMetadata) bool { + return rs.ReferenceID == r.ReferenceID +} + +// RerefenceIDsFilterSpecification is reference IDs based Filter Spec +type RerefenceIDsFilterSpecification struct { + ReferenceIDs []string +} + +// IsSatisfied implementation for reference IDs based Filter spec +func (rs RerefenceIDsFilterSpecification) IsSatisfied(r *policy.RegoMetadata) bool { + // when reference ID's are not specified (could be skip or scan rules), + // return true + if len(rs.ReferenceIDs) < 1 { + return true + } + isSatisfied := false + for _, refID := range rs.ReferenceIDs { + rfIDSpec := RerefenceIDFilterSpecification{refID} + if rfIDSpec.IsSatisfied(r) { + isSatisfied = true + break + } + } + return isSatisfied +} + +// CategoryFilterSpecification is categories based Filter Spec +type CategoryFilterSpecification struct { + categories []string +} + +// IsSatisfied implementation for category based Filter spec +func (c CategoryFilterSpecification) IsSatisfied(r *policy.RegoMetadata) bool { + // when categories are not specified, return true + if len(c.categories) < 1 { + return true + } + return utils.CheckCategory(r.Category, c.categories) +} + +// SeverityFilterSpecification is severity based Filter Spec +type SeverityFilterSpecification struct { + severity string +} + +// IsSatisfied implementation for severity based Filter spec +func (s SeverityFilterSpecification) IsSatisfied(r *policy.RegoMetadata) bool { + // when severity is not specified, return true + if len(s.severity) < 1 { + return true + } + return utils.CheckSeverity(r.Severity, s.severity) +} + +// AndFilterSpecification is a logical AND Filter spec which +// determines if a list of filter specs satisfy the condition +type AndFilterSpecification struct { + filterSpecs []policy.FilterSpecification +} + +// IsSatisfied implementation for And Filter spec +func (a AndFilterSpecification) IsSatisfied(r *policy.RegoMetadata) bool { + if len(a.filterSpecs) < 1 { + return false + } + for _, filterSpec := range a.filterSpecs { + if !filterSpec.IsSatisfied(r) { + return false + } + } + return true +} diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go new file mode 100644 index 000000000..aec027b2c --- /dev/null +++ b/pkg/filters/filters.go @@ -0,0 +1,88 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package filters + +import ( + "github.com/accurics/terrascan/pkg/policy" +) + +// RegoMetadataPreLoadFilter is a pre load filter +// this filter would be while the policy files are processed by policy engine +type RegoMetadataPreLoadFilter struct { + scanRules []string + skipRules []string + categories []string + policyTypes []string + severity string + filterSpecs []policy.FilterSpecification +} + +// NewRegoMetadataPreLoadFilter is a constructor func for RegoMetadataPreLoadFilter +func NewRegoMetadataPreLoadFilter(scanRules, skipRules, categories, policyTypes []string, severity string) *RegoMetadataPreLoadFilter { + return &RegoMetadataPreLoadFilter{ + scanRules: scanRules, + skipRules: skipRules, + categories: categories, + policyTypes: policyTypes, + severity: severity, + // add applicable filter specs to the list + filterSpecs: []policy.FilterSpecification{ + RerefenceIDsFilterSpecification{scanRules}, + CategoryFilterSpecification{categories: categories}, + SeverityFilterSpecification{severity: severity}, + PolicyTypesFilterSpecification{policyTypes: policyTypes}, + }, + } +} + +// IsFiltered checks whether a RegoMetada should be filtered or not +func (r *RegoMetadataPreLoadFilter) IsFiltered(regoMetadata *policy.RegoMetadata) bool { + // if skip rules are specified, RegoMetada is not filtered + if len(r.skipRules) < 1 { + return false + } + refIDsSpec := RerefenceIDsFilterSpecification{r.skipRules} + return refIDsSpec.IsSatisfied(regoMetadata) +} + +// IsAllowed checks whether a RegoMetada should be allowed or not +func (r *RegoMetadataPreLoadFilter) IsAllowed(regoMetadata *policy.RegoMetadata) bool { + andSpec := AndFilterSpecification{r.filterSpecs} + return andSpec.IsSatisfied(regoMetadata) +} + +// RegoDataFilter is a pre scan filter, +// it will be used by policy engine before the evaluation of resources start +type RegoDataFilter struct{} + +// Filter func will filter based on resource type +func (r *RegoDataFilter) Filter(rmap map[string]*policy.RegoData, input policy.EngineInput) map[string]*policy.RegoData { + // if resource config is empty, return original map + if len(*input.InputData) < 1 { + return rmap + } + tempMap := make(map[string]*policy.RegoData) + for resType := range *input.InputData { + for k := range rmap { + resFilterSpec := ResourceTypeFilterSpecification{resType} + if resFilterSpec.IsSatisfied(&rmap[k].Metadata) { + tempMap[k] = rmap[k] + } + } + } + return tempMap +} diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go new file mode 100644 index 000000000..73ebc8d5a --- /dev/null +++ b/pkg/filters/filters_test.go @@ -0,0 +1,358 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package filters + +import ( + "testing" + + "github.com/accurics/terrascan/pkg/iac-providers/output" + "github.com/accurics/terrascan/pkg/policy" + "github.com/accurics/terrascan/pkg/utils" +) + +func TestRegoMetadataPreLoadFilterIsFiltered(t *testing.T) { + testRuleID := "Rule.1" + + type fields struct { + skipRules []string + } + type args struct { + regoMetadata *policy.RegoMetadata + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "no skip rules", + args: args{ + regoMetadata: &policy.RegoMetadata{ + ReferenceID: testRuleID, + }, + }, + want: false, + }, + { + name: "skip rules not matching with metadata reference id", + fields: fields{ + skipRules: []string{"Rule.2", "Rule.3", "Rule.4"}, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + ReferenceID: testRuleID, + }, + }, + want: false, + }, + { + name: "skip rules contain a reference id matching with metadata reference id", + fields: fields{ + skipRules: []string{"Rule.2", "Rule.3", testRuleID}, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + ReferenceID: testRuleID, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewRegoMetadataPreLoadFilter(nil, tt.fields.skipRules, nil, nil, "") + if got := r.IsFiltered(tt.args.regoMetadata); got != tt.want { + t.Errorf("RegoMetadataPreLoadFilter.IsFiltered() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRegoMetadataPreLoadFilterIsAllowed(t *testing.T) { + testRuleID := "Rule.1" + testCategory := "Category.1" + + type fields struct { + scanRules []string + categories []string + policyTypes []string + severity string + } + type args struct { + regoMetadata *policy.RegoMetadata + } + tests := []struct { + name string + fields fields + args args + want bool + noFilterSpecs bool + }{ + { + // when no values are present, all regometadata are allowed + name: "no scan rules, categories or severity specified", + args: args{ + regoMetadata: &policy.RegoMetadata{}, + }, + want: true, + }, + { + name: "only scan rules specified, regometadata referecen id doesn't match", + fields: fields{ + scanRules: []string{testRuleID}, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + ReferenceID: "Rule.2", + }, + }, + want: false, + }, + { + name: "only scan rules specified, regometadata referecen id matches one of the scan rule id", + fields: fields{ + scanRules: []string{testRuleID}, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + ReferenceID: testRuleID, + }, + }, + want: true, + }, + { + name: "only categories specified, regometadata category doesn't match", + fields: fields{ + categories: []string{testCategory}, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + Category: "Category.2", + }, + }, + want: false, + }, + { + name: "only categories specified, regometadata category matches one of the category", + fields: fields{ + categories: []string{testCategory}, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + Category: testCategory, + }, + }, + want: true, + }, + { + name: "only severity specified, regometadata severity doesn't match", + fields: fields{ + severity: utils.HighSeverity, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + Severity: utils.LowSeverity, + }, + }, + want: false, + }, + { + name: "only severity specified, regometadata severity matches one of the severity specified", + fields: fields{ + severity: utils.HighSeverity, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + Severity: utils.HighSeverity, + }, + }, + want: true, + }, + { + name: "only policyTypes specified, regometadata policy type doesn't match", + fields: fields{ + policyTypes: []string{"k8s"}, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + PolicyType: "aws", + }, + }, + want: false, + }, + { + name: "only policyTypes specified, regometadata policy matches one of the policy specified", + fields: fields{ + policyTypes: []string{"azure"}, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + PolicyType: "azure", + }, + }, + want: true, + }, + { + name: "all fields specified, regometadata matches all the values specified", + fields: fields{ + scanRules: []string{testRuleID, "Rule.2"}, + categories: []string{testCategory, "Category.2"}, + policyTypes: []string{"k8s", "aws"}, + severity: utils.HighSeverity, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + ReferenceID: testRuleID, + Category: testCategory, + PolicyType: "aws", + Severity: utils.HighSeverity, + }, + }, + want: true, + }, + { + name: "all fields specified, regometadata doesn't match with one of the values specified", + fields: fields{ + scanRules: []string{testRuleID, "Rule.2"}, + categories: []string{testCategory, "Category.2"}, + policyTypes: []string{"k8s", "aws"}, + severity: utils.HighSeverity, + }, + args: args{ + regoMetadata: &policy.RegoMetadata{ + ReferenceID: testRuleID, + Category: testCategory, + PolicyType: "gcp", + Severity: utils.HighSeverity, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewRegoMetadataPreLoadFilter(tt.fields.scanRules, nil, tt.fields.categories, tt.fields.policyTypes, tt.fields.severity) + if got := r.IsAllowed(tt.args.regoMetadata); got != tt.want { + t.Errorf("RegoMetadataPreLoadFilter.IsAllowed() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRegoDataFilter_Filter(t *testing.T) { + testRegoDataMap := map[string]*policy.RegoData{ + "Rule.1": {}, + "Rule.2": {}, + "Rule.3": {}, + } + + testRegoDataMapWithResourceType := map[string]*policy.RegoData{ + "Rule.1": { + Metadata: policy.RegoMetadata{ + ResourceType: "kubernetes_pod", + }, + }, + "Rule.2": { + Metadata: policy.RegoMetadata{ + ResourceType: "ec2_instance", + }, + }, + "Rule.3": { + Metadata: policy.RegoMetadata{ + ResourceType: "kubernetes_pod", + }, + }, + } + + type args struct { + rmap map[string]*policy.RegoData + input policy.EngineInput + } + tests := []struct { + name string + args args + want map[string]*policy.RegoData + }{ + { + name: "config input doesn't have any resources", + args: args{ + rmap: testRegoDataMap, + input: policy.EngineInput{ + InputData: &output.AllResourceConfigs{}, + }, + }, + want: testRegoDataMap, + }, + { + name: "config input has resources but regometadata doesn't have resource type set", + args: args{ + rmap: testRegoDataMap, + input: policy.EngineInput{ + InputData: &output.AllResourceConfigs{ + "pod": []output.ResourceConfig{}, + }, + }, + }, + want: testRegoDataMap, + }, + { + name: "config input has resources and there are policies matching the type", + args: args{ + rmap: testRegoDataMapWithResourceType, + input: policy.EngineInput{ + InputData: &output.AllResourceConfigs{ + "kubernetes_pod": []output.ResourceConfig{}, + }, + }, + }, + want: map[string]*policy.RegoData{ + "Rule.1": { + Metadata: policy.RegoMetadata{ + ResourceType: "kubernetes_pod", + }, + }, + "Rule.3": { + Metadata: policy.RegoMetadata{ + ResourceType: "kubernetes_pod", + }, + }, + }, + }, + { + name: "config input has resources but there are no policies matching the type", + args: args{ + rmap: testRegoDataMapWithResourceType, + input: policy.EngineInput{ + InputData: &output.AllResourceConfigs{ + "kubernetes_deployment": []output.ResourceConfig{}, + }, + }, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &RegoDataFilter{} + got := r.Filter(tt.args.rmap, tt.args.input) + if len(got) != len(tt.want) { + t.Errorf("RegoDataFilter.Filter() = got size of map %v, want size of map %v", len(got), len(tt.want)) + } + }) + } +} diff --git a/pkg/policy/interface.go b/pkg/policy/interface.go index 1244d57a4..a1714e7bf 100644 --- a/pkg/policy/interface.go +++ b/pkg/policy/interface.go @@ -18,11 +18,27 @@ package policy // Engine Policy Engine interface type Engine interface { - //Init method to initialize engine with policy path, scan and skip rules, and severity level - Init(string, []string, []string, []string, string) error - FilterRules(string, []string, []string, []string, string) + //Init method to initialize engine with policy path, and a pre load filter + Init(string, PreLoadFilter) error Configure() error - Evaluate(EngineInput) (EngineOutput, error) + Evaluate(EngineInput, PreScanFilter) (EngineOutput, error) GetResults() EngineOutput Release() error } + +// FilterSpecification defines a function that +// RegoMetadata filter specifications should implement +type FilterSpecification interface { + IsSatisfied(r *RegoMetadata) bool +} + +// PreLoadFilter defines functions, that a pre load filter should implement +type PreLoadFilter interface { + IsAllowed(r *RegoMetadata) bool + IsFiltered(r *RegoMetadata) bool +} + +// PreScanFilter defines function, that a pre scan filter should implement +type PreScanFilter interface { + Filter(rmap map[string]*RegoData, input EngineInput) map[string]*RegoData +} diff --git a/pkg/policy/opa/engine.go b/pkg/policy/opa/engine.go index c07d624eb..f43412cc5 100644 --- a/pkg/policy/opa/engine.go +++ b/pkg/policy/opa/engine.go @@ -55,7 +55,7 @@ func NewEngine() (*Engine, error) { } // LoadRegoMetadata Loads rego metadata from a given file -func (e *Engine) LoadRegoMetadata(metaFilename string) (*RegoMetadata, error) { +func (e *Engine) LoadRegoMetadata(metaFilename string) (*policy.RegoMetadata, error) { // Load metadata file if it exists metadata, err := ioutil.ReadFile(metaFilename) if err != nil { @@ -66,7 +66,7 @@ func (e *Engine) LoadRegoMetadata(metaFilename string) (*RegoMetadata, error) { } // Read metadata into struct - regoMetadata := RegoMetadata{} + regoMetadata := policy.RegoMetadata{} if err = json.Unmarshal(metadata, ®oMetadata); err != nil { zap.S().Error("failed to unmarshal rego metadata", zap.String("file", metaFilename), zap.Error(err)) return nil, err @@ -75,7 +75,7 @@ func (e *Engine) LoadRegoMetadata(metaFilename string) (*RegoMetadata, error) { } // loadRawRegoFilesIntoMap imports raw rego files into a map -func (e *Engine) loadRawRegoFilesIntoMap(currentDir string, regoDataList []*RegoData, regoFileMap *map[string][]byte) error { +func (e *Engine) loadRawRegoFilesIntoMap(currentDir string, regoDataList []*policy.RegoData, regoFileMap *map[string][]byte) error { for i := range regoDataList { regoPath := filepath.Join(currentDir, regoDataList[i].Metadata.File) rawRegoData, err := ioutil.ReadFile(regoPath) @@ -98,7 +98,7 @@ func (e *Engine) loadRawRegoFilesIntoMap(currentDir string, regoDataList []*Rego } // LoadRegoFiles Loads all related rego files from the given policy path into memory -func (e *Engine) LoadRegoFiles(policyPath string) error { +func (e *Engine) LoadRegoFiles(policyPath string, filter policy.PreLoadFilter) error { // Walk the file path and find all directories dirList, err := utils.FindAllDirectories(policyPath) if err != nil { @@ -110,7 +110,7 @@ func (e *Engine) LoadRegoFiles(policyPath string) error { } e.regoFileMap = make(map[string][]byte) - e.regoDataMap = make(map[string]*RegoData) + e.regoDataMap = make(map[string]*policy.RegoData) // Load rego data files from each dir // First, we read the metadata file, which contains info about the associated rego rule. The .rego file data is @@ -134,17 +134,29 @@ func (e *Engine) LoadRegoFiles(policyPath string) error { continue } - var regoDataList []*RegoData + var regoDataList []*policy.RegoData for j := range metadataFiles { filePath := filepath.Join(dirList[i], *metadataFiles[j]) - var regoMetadata *RegoMetadata + var regoMetadata *policy.RegoMetadata regoMetadata, err = e.LoadRegoMetadata(filePath) if err != nil { zap.S().Error("error loading rego metadata", zap.String("file", filePath), zap.Error(err)) continue } + // check if the rego metadata is allowed + // this check is for scan rules, categories, policy types, and severity + if !filter.IsAllowed(regoMetadata) { + continue + } + + // check if the rego metadata should be filtered + // this check is for skip rules + if filter.IsFiltered(regoMetadata) { + continue + } + // Perform some sanity checks if strings.Contains(regoMetadata.Name, ".") { zap.S().Error("error loading rego metadata: rule name must not contain a dot character", zap.String("name", regoMetadata.Name), zap.String("file", filePath)) @@ -160,7 +172,7 @@ func (e *Engine) LoadRegoFiles(policyPath string) error { regoMetadata.TemplateArgs["name"] = regoMetadata.Name } - regoData := RegoData{ + regoData := policy.RegoData{ Metadata: *regoMetadata, } @@ -242,20 +254,14 @@ func (e *Engine) CompileRegoFiles() error { // Init initializes the Opa engine // Handles loading all rules, filtering, compiling, and preparing for evaluation -func (e *Engine) Init(policyPath string, scanRules, skipRules, categories []string, severity string) error { +func (e *Engine) Init(policyPath string, filter policy.PreLoadFilter) error { e.context = context.Background() - if err := e.LoadRegoFiles(policyPath); err != nil { + if err := e.LoadRegoFiles(policyPath, filter); err != nil { zap.S().Error("error loading rego files", zap.String("policy path", policyPath), zap.Error(err)) return ErrInitFailed } - // before compiling the rego files, filter the rules based on scan and skip rules, and severity level supplied - e.FilterRules(policyPath, scanRules, skipRules, categories, severity) - - // update the rule count - e.stats.ruleCount = len(e.regoDataMap) - err := e.CompileRegoFiles() if err != nil { zap.S().Error("error compiling rego files", zap.String("policy path", policyPath), zap.Error(err)) @@ -284,7 +290,7 @@ func (e *Engine) Release() error { } // reportViolation Add a violation for a given resource -func (e *Engine) reportViolation(regoData *RegoData, resource *output.ResourceConfig, isSkipped bool, skipComment string) { +func (e *Engine) reportViolation(regoData *policy.RegoData, resource *output.ResourceConfig, isSkipped bool, skipComment string) { violation := results.Violation{ RuleName: regoData.Metadata.Name, Description: regoData.Metadata.Description, @@ -333,7 +339,7 @@ func (e *Engine) reportViolation(regoData *RegoData, resource *output.ResourceCo } // reportPassed Adds a passed rule which wasn't violated by all the resources -func (e *Engine) reportPassed(regoData *RegoData) { +func (e *Engine) reportPassed(regoData *policy.RegoData) { passedRule := results.PassedRule{ RuleName: regoData.Metadata.Name, Description: regoData.Metadata.Description, @@ -346,15 +352,22 @@ func (e *Engine) reportPassed(regoData *RegoData) { } // Evaluate Executes compiled OPA queries against the input JSON data -func (e *Engine) Evaluate(engineInput policy.EngineInput) (policy.EngineOutput, error) { +func (e *Engine) Evaluate(engineInput policy.EngineInput, filter policy.PreScanFilter) (policy.EngineOutput, error) { // Keep track of how long it takes to evaluate the policies start := time.Now() + e.regoDataMap = filter.Filter(e.regoDataMap, engineInput) + + // update the rule count + e.stats.ruleCount = len(e.regoDataMap) + // Evaluate the policy against each resource type for k := range e.regoDataMap { // Execute the prepared query. rs, err := e.regoDataMap[k].PreparedQuery.Eval(e.context, rego.EvalInput(engineInput.InputData)) if err != nil { + // since the eval failed with the policy, we should decrement the total count by 1 + e.stats.ruleCount-- zap.S().Debug("failed to run prepared query", zap.Error(err), zap.String("rule", "'"+k+"'"), zap.String("file", e.regoDataMap[k].Metadata.File)) continue } @@ -451,95 +464,3 @@ func (e *Engine) Evaluate(engineInput policy.EngineInput) (policy.EngineOutput, e.results.ViolationStore.Summary.TotalTime += int64(e.stats.runTime) return e.results, nil } - -// FilterRules will apply the scan and skip rules, severity level and categories -func (e *Engine) FilterRules(policyPath string, scanRules, skipRules, categories []string, severity string) { - // apply scan rules - if len(scanRules) > 0 { - e.filterScanRules(policyPath, scanRules) - } - - // apply skip rules - if len(skipRules) > 0 { - e.filterSkipRules(policyPath, skipRules) - } - - // apply categories - if len(categories) > 0 { - e.filterByCategories(policyPath, categories) - } - - // apply severity - if len(severity) > 0 { - e.filterBySeverity(policyPath, severity) - } -} - -func (e *Engine) filterScanRules(policyPath string, scanRules []string) { - - // temporary map to store data from original rego data map - tempMap := make(map[string]*RegoData) - for _, ruleID := range scanRules { - regoData, ok := e.regoDataMap[ruleID] - if ok { - zap.S().Infof("scan rule added. rule id: %+v found in policy path: %s", ruleID, policyPath) - tempMap[ruleID] = regoData - } else { - zap.S().Warnf("scan rule id: %+v not found in policy path: %s", ruleID, policyPath) - } - } - if len(tempMap) == 0 { - zap.S().Warnf("scan rule id's: %+v not found in policy path: %s", scanRules, policyPath) - } - - // the regoDataMap should only contain regoData for supplied scan rules - e.regoDataMap = tempMap -} - -func (e *Engine) filterSkipRules(policyPath string, skipRules []string) { - // remove rules to be skipped from the rego data map - for _, ruleID := range skipRules { - _, ok := e.regoDataMap[ruleID] - if ok { - zap.S().Infof("skip rule added. rule id: %+v found in policy path: %s", ruleID, policyPath) - delete(e.regoDataMap, ruleID) - } else { - zap.S().Warnf("skip rule id: %+v not found in policy path: %s", ruleID, policyPath) - } - } -} - -func (e *Engine) filterByCategories(policyPath string, categories []string) { - - // temporary map to store data from original rego data map - tempMap := make(map[string]*RegoData) - for ruleID, regoData := range e.regoDataMap { - - if utils.CheckCategory(regoData.Metadata.Category, categories) { - tempMap[ruleID] = regoData - } - } - if len(tempMap) == 0 { - zap.S().Debugf("policy path: %s, doesn't have any rule matching the categories : %v", policyPath, categories) - } - - // the regoDataMap should only contain regoData for required minimum severity level - e.regoDataMap = tempMap -} - -func (e *Engine) filterBySeverity(policyPath string, severity string) { - // temporary map to store data from original rego data map - tempMap := make(map[string]*RegoData) - for ruleID, regoData := range e.regoDataMap { - - if utils.CheckSeverity(regoData.Metadata.Severity, severity) { - tempMap[ruleID] = regoData - } - } - if len(tempMap) == 0 { - zap.S().Debugf("policy path: %s, doesn't have any rule matching the severity level : %s", policyPath, severity) - } - - // the regoDataMap should only contain regoData for required minimum severity level - e.regoDataMap = tempMap -} diff --git a/pkg/policy/opa/engine_test.go b/pkg/policy/opa/engine_test.go deleted file mode 100644 index 1c9d8e4d4..000000000 --- a/pkg/policy/opa/engine_test.go +++ /dev/null @@ -1,398 +0,0 @@ -package opa - -import ( - "fmt" - "testing" - - "github.com/accurics/terrascan/pkg/utils" -) - -var testPolicyPath string = "test" - -type args struct { - policyPath string - scanRules []string - skipRules []string - categories []string - severity string -} - -func TestFilterRules(t *testing.T) { - - tests := []struct { - name string - args args - assert bool - regoMapSize int - regoDataMap map[string]*RegoData - }{ - { - name: "no scan and skip rules", - args: args{}, - regoDataMap: nil, - }, - { - name: "scan rules test", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.0", "Rule.1", "Rule.2", "Rule.3", "Rule.10"}, - }, - regoDataMap: getTestRegoDataMap(10), - assert: true, - regoMapSize: 4, - }, - { - name: "scan rules not found in path", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.11", "Rule.12", "Rule.13"}, - }, - regoDataMap: getTestRegoDataMap(10), - assert: true, - regoMapSize: 0, - }, - { - name: "skip rules test", - args: args{ - policyPath: testPolicyPath, - skipRules: []string{"Rule.1"}, - }, - regoDataMap: getTestRegoDataMap(6), - assert: true, - regoMapSize: 5, - }, - { - name: "skip rules not found in policy path", - args: args{ - policyPath: testPolicyPath, - skipRules: []string{"Rule.21", "Rule.22"}, - }, - regoDataMap: getTestRegoDataMap(20), - assert: true, - regoMapSize: 20, - }, - { - name: "both scan and skip rules supplied", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - }, - regoDataMap: getTestRegoDataMap(50), - assert: true, - regoMapSize: 7, - }, - { - name: "both scan and skip rules supplied, with desired severity : low and rule severity : blank", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - severity: "LOW", - }, - regoDataMap: getTestRegoDataMap(50), - assert: true, - regoMapSize: 7, - }, - { - name: "both scan and skip rules supplied, with desired severity : high and rule severity : high", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - severity: "HIGH", - }, - regoDataMap: getTestRegoDataMapHighSeverity(50), - assert: true, - regoMapSize: 7, - }, - { - name: "both scan and skip rules supplied, with desired severity : high and rule severity : high", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - severity: "HIGH", - }, - regoDataMap: getTestRegoDataMapLowSeverity(50), - assert: true, - regoMapSize: 0, - }, - { - name: "both scan and skip rules supplied, with desired severity : high and rule severity : medium", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - severity: "high", - }, - regoDataMap: getTestRegoDataMapMediumSeverity(50), - assert: true, - regoMapSize: 0, - }, - { - name: "both scan and skip rules supplied, with desired severity : medium and rule severity : low", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - severity: "MEDIUM", - }, - regoDataMap: getTestRegoDataMapLowSeverity(50), - assert: true, - regoMapSize: 0, - }, - { - name: "both scan and skip rules supplied, with desired severity : medium and rule severity : medium", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - severity: "MEDIUM", - }, - regoDataMap: getTestRegoDataMapMediumSeverity(50), - assert: true, - regoMapSize: 7, - }, - { - name: "both scan and skip rules supplied, with desired severity : low and rule severity : low", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - severity: "low", - }, - regoDataMap: getTestRegoDataMapLowSeverity(50), - assert: true, - regoMapSize: 7, - }, - { - name: "both scan and skip rules supplied, with desired category : COMPLIANCE VALIDATION", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - categories: []string{"COMPLIANCE VALIDATION"}, - }, - regoDataMap: getTestRegoDataMapCVCategory(50), - assert: true, - regoMapSize: 7, - }, - { - name: "both scan and skip rules supplied, with desired category : DATA PROTECTION", - args: args{ - policyPath: testPolicyPath, - scanRules: []string{"Rule.6", "Rule.7", "Rule.8", "Rule.15", "Rule.31", "Rule.32", "Rule.40", "Rule.41", "Rule.42"}, - skipRules: []string{"Rule.31", "Rule.32", "Rule.38"}, - categories: []string{"DATA PROTECTION"}, - }, - regoDataMap: getTestRegoDataMapDPCategory(50), - assert: true, - regoMapSize: 7, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := &Engine{ - regoDataMap: tt.regoDataMap, - } - e.FilterRules(tt.args.policyPath, tt.args.scanRules, tt.args.skipRules, tt.args.categories, tt.args.severity) - if tt.assert { - if len(e.regoDataMap) != tt.regoMapSize { - t.Errorf("filterRules(): expected regoDataMap size = %d, got = %d", tt.regoMapSize, len(e.regoDataMap)) - } - } - }) - } -} - -func TestFilterRulesWithEquallyDividedSeverity(t *testing.T) { - tests := []struct { - name string - args args - assert bool - regoMapSize int - regoDataMap map[string]*RegoData - }{ - { - name: "no scan and skip rules supplied, with desired severity: low and rules severities: low, medium, high", - args: args{ - policyPath: testPolicyPath, - severity: "low", - }, - regoDataMap: getTestRegoDataMapWithEquallyDividedSeverity(30, []string{"LOW", "MEDIUM", "HIGH"}), - assert: true, - regoMapSize: 30, - }, - { - name: "no scan and skip rules supplied, with desired severity: medium and rules severities: low, medium, high", - args: args{ - policyPath: testPolicyPath, - severity: "medium", - }, - regoDataMap: getTestRegoDataMapWithEquallyDividedSeverity(30, []string{"LOW", "MEDIUM", "HIGH"}), - assert: true, - regoMapSize: 20, - }, - { - name: "no scan and skip rules supplied, with desired severity: high and rules severities: low, medium, high", - args: args{ - policyPath: testPolicyPath, - severity: "high", - }, - regoDataMap: getTestRegoDataMapWithEquallyDividedSeverity(30, []string{"LOW", "MEDIUM", "HIGH"}), - assert: true, - regoMapSize: 10, - }, - { - name: "no scan and skip rules supplied, with desired severity: high and rules severities: low, medium", - args: args{ - policyPath: testPolicyPath, - severity: "high", - }, - regoDataMap: getTestRegoDataMapWithEquallyDividedSeverity(30, []string{"LOW", "MEDIUM"}), - assert: true, - regoMapSize: 0, - }, - { - name: "no scan and skip rules supplied, with desired severity: high and rules severities: low, medium", - args: args{ - policyPath: testPolicyPath, - severity: "MEDIUM", - }, - regoDataMap: getTestRegoDataMapWithEquallyDividedSeverity(30, []string{"LOW", "MEDIUM"}), - assert: true, - regoMapSize: 15, - }, - { - name: "no scan and skip rules supplied, with desired severity: high and rules severities: low, medium", - args: args{ - policyPath: testPolicyPath, - severity: "LOW", - }, - regoDataMap: getTestRegoDataMapWithEquallyDividedSeverity(30, []string{"LOW", "MEDIUM"}), - assert: true, - regoMapSize: 30, - }, - { - name: "no scan and skip rules supplied, with desired severity: low and rules severities: low", - args: args{ - policyPath: testPolicyPath, - severity: "LOW", - }, - regoDataMap: getTestRegoDataMapWithEquallyDividedSeverity(10, []string{"MEDIUM"}), - assert: true, - regoMapSize: 10, - }, - { - name: "no scan and skip rules supplied, with desired severity : medium and rules severities: medium", - args: args{ - policyPath: testPolicyPath, - severity: "MEDIUM", - }, - regoDataMap: getTestRegoDataMapWithEquallyDividedSeverity(10, []string{"MEDIUM"}), - assert: true, - regoMapSize: 10, - }, - { - name: "no scan and skip rules supplied, with desired severity: high and rules severities: medium", - args: args{ - policyPath: testPolicyPath, - severity: "HIGH", - }, - regoDataMap: getTestRegoDataMapWithEquallyDividedSeverity(10, []string{"MEDIUM"}), - assert: true, - regoMapSize: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := &Engine{ - regoDataMap: tt.regoDataMap, - } - e.FilterRules(tt.args.policyPath, tt.args.scanRules, tt.args.skipRules, tt.args.categories, tt.args.severity) - if tt.assert { - if len(e.regoDataMap) != tt.regoMapSize { - t.Errorf("filterRules(): expected regoDataMap size = %d, got = %d", tt.regoMapSize, len(e.regoDataMap)) - } - } - }) - } -} - -// helper func to generate test rego data map of given size -func getTestRegoDataMap(size int) map[string]*RegoData { - testRegoDataMap := make(map[string]*RegoData) - for i := 0; i < size; i++ { - ruleID := fmt.Sprintf("Rule.%d", i) - testRegoDataMap[ruleID] = &RegoData{} - } - return testRegoDataMap -} - -// helper func to generate test rego data map of given size with severity levels attached in equal proportion -func getTestRegoDataMapWithEquallyDividedSeverity(size int, severities []string) map[string]*RegoData { - - severitytypes := len(severities) - - if severitytypes == 0 { - return getTestRegoDataMap(size) - } - - severitypartitionsizes := size / severitytypes - severitycounter := 0 - testRegoDataMap := make(map[string]*RegoData) - - for i := 0; i < size; i++ { - ruleID := fmt.Sprintf("Rule.%d", i) - testRegoDataMap[ruleID] = &RegoData{Metadata: RegoMetadata{Severity: severities[severitycounter]}} - if (i+1)%severitypartitionsizes == 0 { - severitycounter = severitycounter + 1 - } - } - - return testRegoDataMap -} - -// helper func to generate test rego data map of given size with required severity -func getTestRegoDataMapWithSeverity(size int, severity string) map[string]*RegoData { - testRegoDataMap := getTestRegoDataMap(size) - - for _, regoData := range testRegoDataMap { - regoData.Metadata.Severity = severity - } - return testRegoDataMap -} - -func getTestRegoDataMapWithCategory(size int, category string) map[string]*RegoData { - testRegoDataMap := getTestRegoDataMap(size) - - for _, regoData := range testRegoDataMap { - regoData.Metadata.Category = category - } - return testRegoDataMap -} - -// helper func to generate test rego data map of given size with high severity -func getTestRegoDataMapHighSeverity(size int) map[string]*RegoData { - return getTestRegoDataMapWithSeverity(size, utils.HighSeverity) -} - -// helper func to generate test rego data map of given size with medium severity -func getTestRegoDataMapMediumSeverity(size int) map[string]*RegoData { - return getTestRegoDataMapWithSeverity(size, utils.MediumSeverity) -} - -// helper func to generate test rego data map of given size with low severity -func getTestRegoDataMapLowSeverity(size int) map[string]*RegoData { - return getTestRegoDataMapWithSeverity(size, utils.LowSeverity) -} - -func getTestRegoDataMapCVCategory(size int) map[string]*RegoData { - return getTestRegoDataMapWithCategory(size, utils.AcceptedCategories[1]) -} - -func getTestRegoDataMapDPCategory(size int) map[string]*RegoData { - return getTestRegoDataMapWithCategory(size, utils.AcceptedCategories[7]) -} diff --git a/pkg/policy/opa/types.go b/pkg/policy/opa/types.go index ed1bbf00e..253ac20e9 100644 --- a/pkg/policy/opa/types.go +++ b/pkg/policy/opa/types.go @@ -21,29 +21,8 @@ import ( "time" "github.com/accurics/terrascan/pkg/policy" - - "github.com/open-policy-agent/opa/rego" ) -// RegoMetadata The rego metadata struct which is read and saved from disk -type RegoMetadata struct { - Name string `json:"name"` - File string `json:"file"` - TemplateArgs map[string]interface{} `json:"template_args"` - Severity string `json:"severity"` - Description string `json:"description"` - ReferenceID string `json:"reference_id"` - Category string `json:"category"` - Version int `json:"version"` -} - -// RegoData Stores all information needed to evaluate and report on a rego rule -type RegoData struct { - Metadata RegoMetadata - RawRego []byte - PreparedQuery *rego.PreparedEvalQuery -} - // EngineStats Contains misc stats type EngineStats struct { ruleCount int @@ -58,6 +37,6 @@ type Engine struct { results policy.EngineOutput context context.Context regoFileMap map[string][]byte - regoDataMap map[string]*RegoData + regoDataMap map[string]*policy.RegoData stats EngineStats } diff --git a/pkg/policy/types.go b/pkg/policy/types.go index 1b349cd5c..f8aedab42 100644 --- a/pkg/policy/types.go +++ b/pkg/policy/types.go @@ -5,6 +5,7 @@ import ( "github.com/accurics/terrascan/pkg/iac-providers/output" "github.com/accurics/terrascan/pkg/results" + "github.com/open-policy-agent/opa/rego" ) const ( @@ -44,3 +45,24 @@ func (me EngineOutput) AsViolationStore() results.ViolationStore { Summary: me.Summary, } } + +// RegoMetadata The rego metadata struct which is read and saved from disk +type RegoMetadata struct { + Name string `json:"name"` + File string `json:"file"` + PolicyType string `json:"policy_type"` + ResourceType string `json:"resource_type"` + TemplateArgs map[string]interface{} `json:"template_args"` + Severity string `json:"severity"` + Description string `json:"description"` + ReferenceID string `json:"reference_id"` + Category string `json:"category"` + Version int `json:"version"` +} + +// RegoData Stores all information needed to evaluate and report on a rego rule +type RegoData struct { + Metadata RegoMetadata + RawRego []byte + PreparedQuery *rego.PreparedEvalQuery +} diff --git a/pkg/runtime/executor.go b/pkg/runtime/executor.go index 5f5abf412..5f1b74a50 100644 --- a/pkg/runtime/executor.go +++ b/pkg/runtime/executor.go @@ -21,6 +21,7 @@ import ( "go.uber.org/zap" + "github.com/accurics/terrascan/pkg/filters" iacProvider "github.com/accurics/terrascan/pkg/iac-providers" "github.com/accurics/terrascan/pkg/iac-providers/output" "github.com/accurics/terrascan/pkg/notifications" @@ -34,7 +35,6 @@ type Executor struct { filePath string dirPath string policyPath []string - cloudType []string iacType string iacVersion string scanRules []string @@ -43,17 +43,18 @@ type Executor struct { policyEngines []policy.Engine notifiers []notifications.Notifier categories []string + policyTypes []string severity string nonRecursive bool } // NewExecutor creates a runtime object -func NewExecutor(iacType, iacVersion string, cloudType []string, filePath, dirPath string, policyPath, scanRules, skipRules, categories []string, severity string, nonRecursive bool) (e *Executor, err error) { +func NewExecutor(iacType, iacVersion string, policyTypes []string, filePath, dirPath string, policyPath, scanRules, skipRules, categories []string, severity string, nonRecursive bool) (e *Executor, err error) { e = &Executor{ filePath: filePath, dirPath: dirPath, policyPath: policyPath, - cloudType: cloudType, + policyTypes: policyTypes, iacType: iacType, iacVersion: iacVersion, iacProviders: make([]iacProvider.IacProvider, 0), @@ -147,8 +148,11 @@ func (e *Executor) Init() error { return err } + // create a new RegoMetadata pre load filter + preloadFilter := filters.NewRegoMetadataPreLoadFilter(e.scanRules, e.skipRules, e.categories, e.policyTypes, e.severity) + // initialize the engine - if err := engine.Init(policyPath, e.scanRules, e.skipRules, e.categories, e.severity); err != nil { + if err := engine.Init(policyPath, preloadFilter); err != nil { zap.S().Errorf("%s", err) return err } @@ -264,7 +268,7 @@ func (e *Executor) findViolations(results *Output) error { for _, engine := range e.policyEngines { go func(eng policy.Engine) { - output, err := eng.Evaluate(policy.EngineInput{InputData: &results.ResourceConfig}) + output, err := eng.Evaluate(policy.EngineInput{InputData: &results.ResourceConfig}, &filters.RegoDataFilter{}) evalResultChan <- engineEvalResult{err, output} }(engine) } diff --git a/pkg/runtime/executor_test.go b/pkg/runtime/executor_test.go index 221dea24a..28fa277a8 100644 --- a/pkg/runtime/executor_test.go +++ b/pkg/runtime/executor_test.go @@ -68,7 +68,7 @@ type MockPolicyEngine struct { err error } -func (m MockPolicyEngine) Init(input string, scanRules, skipRules, categories []string, severity string) error { +func (m MockPolicyEngine) Init(input string, filter policy.PreLoadFilter) error { return m.err } @@ -82,7 +82,7 @@ func (m MockPolicyEngine) Configure() error { return m.err } -func (m MockPolicyEngine) Evaluate(input policy.EngineInput) (out policy.EngineOutput, err error) { +func (m MockPolicyEngine) Evaluate(input policy.EngineInput, filter policy.PreScanFilter) (out policy.EngineOutput, err error) { return out, m.err } @@ -197,12 +197,12 @@ func TestInit(t *testing.T) { { name: "valid filePath", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", - policyPath: []string{testPoliciesDir}, + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", + policyPath: []string{testPoliciesDir}, }, wantErr: nil, wantIacProvider: []iacProvider.IacProvider{&tfv14.TfV14{}}, @@ -211,9 +211,9 @@ func TestInit(t *testing.T) { { name: "empty iac type with -d flag", executor: Executor{ - dirPath: testDataDir, - cloudType: []string{"aws"}, - policyPath: []string{testPoliciesDir}, + dirPath: testDataDir, + policyTypes: []string{"aws"}, + policyPath: []string{testPoliciesDir}, }, wantErr: nil, wantIacProvider: []iacProvider.IacProvider{&cftv1.CFTV1{}, &helmv3.HelmV3{}, &k8sv1.K8sV1{}, &kustomizev3.KustomizeV3{}, &tfv14.TfV14{}}, @@ -222,9 +222,9 @@ func TestInit(t *testing.T) { { name: "empty iac type with -f flag", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - cloudType: []string{"aws"}, - policyPath: []string{testPoliciesDir}, + filePath: filepath.Join(testDataDir, "testfile"), + policyTypes: []string{"aws"}, + policyPath: []string{testPoliciesDir}, }, wantErr: nil, wantIacProvider: []iacProvider.IacProvider{&tfv14.TfV14{}}, @@ -233,12 +233,12 @@ func TestInit(t *testing.T) { { name: "valid notifier", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", - policyPath: []string{testPoliciesDir}, + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", + policyPath: []string{testPoliciesDir}, }, configFile: filepath.Join(testDataDir, "webhook.toml"), wantErr: nil, @@ -248,11 +248,11 @@ func TestInit(t *testing.T) { { name: "invalid notifier", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", }, configFile: filepath.Join(testDataDir, "invalid-notifier.toml"), wantErr: fmt.Errorf("notifier not supported"), @@ -262,11 +262,11 @@ func TestInit(t *testing.T) { { name: "config not present", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", }, configFile: filepath.Join(testDataDir, "does-not-exist"), wantErr: config.ErrNotPresent, @@ -275,12 +275,12 @@ func TestInit(t *testing.T) { { name: "invalid policy path", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", - policyPath: []string{filepath.Join(testDataDir, "notthere")}, + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", + policyPath: []string{filepath.Join(testDataDir, "notthere")}, }, configFile: filepath.Join(testDataDir, "webhook.toml"), wantErr: fmt.Errorf("failed to initialize OPA policy engine"), @@ -290,12 +290,12 @@ func TestInit(t *testing.T) { { name: "config file with invalid category", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", - policyPath: []string{filepath.Join(testDataDir, "notthere")}, + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", + policyPath: []string{filepath.Join(testDataDir, "notthere")}, }, configFile: filepath.Join(testDataDir, "invalid-category.toml"), wantErr: fmt.Errorf("(3, 5): no value can start with c"), @@ -304,12 +304,12 @@ func TestInit(t *testing.T) { { name: "valid filePath", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v12", - policyPath: []string{testPoliciesDir}, + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v12", + policyPath: []string{testPoliciesDir}, }, wantErr: nil, wantIacProvider: []iacProvider.IacProvider{&tfv12.TfV12{}}, @@ -318,12 +318,12 @@ func TestInit(t *testing.T) { { name: "valid notifier", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v12", - policyPath: []string{testPoliciesDir}, + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v12", + policyPath: []string{testPoliciesDir}, }, configFile: filepath.Join(testDataDir, "webhook.toml"), wantErr: nil, @@ -333,11 +333,11 @@ func TestInit(t *testing.T) { { name: "invalid notifier", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v12", + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v12", }, configFile: filepath.Join(testDataDir, "invalid-notifier.toml"), wantErr: fmt.Errorf("notifier not supported"), @@ -347,11 +347,11 @@ func TestInit(t *testing.T) { { name: "config not present", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v12", + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v12", }, configFile: filepath.Join(testDataDir, "does-not-exist"), wantErr: config.ErrNotPresent, @@ -360,12 +360,12 @@ func TestInit(t *testing.T) { { name: "invalid policy path", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v12", - policyPath: []string{filepath.Join(testDataDir, "notthere")}, + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v12", + policyPath: []string{filepath.Join(testDataDir, "notthere")}, }, configFile: filepath.Join(testDataDir, "webhook.toml"), wantErr: fmt.Errorf("failed to initialize OPA policy engine"), @@ -403,16 +403,16 @@ func TestInit(t *testing.T) { } type flagSet struct { - iacType string - iacVersion string - filePath string - dirPath string - policyPath []string - cloudType []string - categories []string - severity string - scanRules []string - skipRules []string + iacType string + iacVersion string + filePath string + dirPath string + policyPath []string + policyTypes []string + categories []string + severity string + scanRules []string + skipRules []string } func TestNewExecutor(t *testing.T) { @@ -431,12 +431,12 @@ func TestNewExecutor(t *testing.T) { configfile: filepath.Join(testDataDir, "scan-skip-rules-low-severity.toml"), wantErr: nil, flags: flagSet{ - severity: "high", - scanRules: []string{"AWS.S3Bucket.DS.High.1043"}, - skipRules: []string{"accurics.kubernetes.IAM.109"}, - dirPath: testDir, - policyPath: []string{testPoliciesDir}, - cloudType: []string{"aws"}, + severity: "high", + scanRules: []string{"AWS.S3Bucket.DS.High.1043"}, + skipRules: []string{"accurics.kubernetes.IAM.109"}, + dirPath: testDir, + policyPath: []string{testPoliciesDir}, + policyTypes: []string{"aws"}, }, wantScanRules: []string{ "AWS.S3Bucket.DS.High.1043", @@ -452,10 +452,10 @@ func TestNewExecutor(t *testing.T) { configfile: filepath.Join(testDataDir, "scan-skip-rules-low-severity.toml"), wantErr: nil, flags: flagSet{ - skipRules: []string{"accurics.kubernetes.IAM.109"}, - dirPath: testDir, - policyPath: []string{testPoliciesDir}, - cloudType: []string{"aws"}, + skipRules: []string{"accurics.kubernetes.IAM.109"}, + dirPath: testDir, + policyPath: []string{testPoliciesDir}, + policyTypes: []string{"aws"}, }, wantScanRules: []string{ "AWS.S3Bucket.DS.High.1043", @@ -472,10 +472,10 @@ func TestNewExecutor(t *testing.T) { configfile: filepath.Join(testDataDir, "scan-skip-rules-low-severity.toml"), wantErr: nil, flags: flagSet{ - scanRules: []string{"AWS.S3Bucket.DS.High.1043"}, - dirPath: testDir, - policyPath: []string{testPoliciesDir}, - cloudType: []string{"aws"}, + scanRules: []string{"AWS.S3Bucket.DS.High.1043"}, + dirPath: testDir, + policyPath: []string{testPoliciesDir}, + policyTypes: []string{"aws"}, }, wantScanRules: []string{ "AWS.S3Bucket.DS.High.1043", @@ -494,11 +494,11 @@ func TestNewExecutor(t *testing.T) { configfile: filepath.Join(testDataDir, "scan-skip-rules-low-severity.toml"), wantErr: nil, flags: flagSet{ - severity: "medium", - dirPath: testDataDir, - policyPath: []string{testPoliciesDir}, - cloudType: []string{"aws"}, - categories: []string{"DATA PROTECTION"}, + severity: "medium", + dirPath: testDataDir, + policyPath: []string{testPoliciesDir}, + policyTypes: []string{"aws"}, + categories: []string{"DATA PROTECTION"}, }, wantScanRules: []string{ "AWS.S3Bucket.DS.High.1043", @@ -518,9 +518,9 @@ func TestNewExecutor(t *testing.T) { configfile: filepath.Join(testDataDir, "scan-skip-rules-low-severity.toml"), wantErr: nil, flags: flagSet{ - dirPath: testDir, - policyPath: []string{testPoliciesDir}, - cloudType: []string{"aws"}, + dirPath: testDir, + policyPath: []string{testPoliciesDir}, + policyTypes: []string{"aws"}, }, wantScanRules: []string{ "AWS.S3Bucket.DS.High.1043", @@ -541,7 +541,7 @@ func TestNewExecutor(t *testing.T) { t.Run(tt.name, func(t *testing.T) { config.LoadGlobalConfig(tt.configfile) - gotExecutor, gotErr := NewExecutor(tt.flags.iacType, tt.flags.iacVersion, tt.flags.cloudType, tt.flags.filePath, tt.flags.dirPath, tt.flags.policyPath, tt.flags.scanRules, tt.flags.skipRules, tt.flags.categories, tt.flags.severity, false) + gotExecutor, gotErr := NewExecutor(tt.flags.iacType, tt.flags.iacVersion, tt.flags.policyTypes, tt.flags.filePath, tt.flags.dirPath, tt.flags.policyPath, tt.flags.scanRules, tt.flags.skipRules, tt.flags.categories, tt.flags.severity, false) if !reflect.DeepEqual(tt.wantErr, gotErr) { t.Errorf("Mismatch in error => got: '%v', want: '%v'", gotErr, tt.wantErr) diff --git a/pkg/runtime/validate.go b/pkg/runtime/validate.go index af7a117e3..88992afb2 100644 --- a/pkg/runtime/validate.go +++ b/pkg/runtime/validate.go @@ -102,8 +102,8 @@ func (e *Executor) ValidateInputs() error { if e.dirPath != "" { e.iacType = "all" } else { - // TODO: handle more than cloudType[0] - e.iacType = policy.GetDefaultIacType(e.cloudType[0]) + // TODO: handle more than policyTypes[0] + e.iacType = policy.GetDefaultIacType(e.policyTypes[0]) } } @@ -114,17 +114,17 @@ func (e *Executor) ValidateInputs() error { } } - // check if cloud type is supported - for _, ct := range e.cloudType { + // check if cloud type(policy type) is supported + for _, ct := range e.policyTypes { if !policy.IsCloudProviderSupported(ct) { zap.S().Errorf("cloud type '%s' not supported", ct) return errCloudNotSupported } } - zap.S().Debugf("cloud type '%s' is supported", strings.Join(e.cloudType, ",")) + zap.S().Debugf("cloud type '%s' is supported", strings.Join(e.policyTypes, ",")) if len(e.policyPath) == 0 { - e.policyPath = policy.GetDefaultPolicyPaths(e.cloudType) + e.policyPath = policy.GetDefaultPolicyPaths(e.policyTypes) } zap.S().Debugf("using policy path %v", e.policyPath) diff --git a/pkg/runtime/validate_test.go b/pkg/runtime/validate_test.go index 3ccb11ab6..edad5adca 100644 --- a/pkg/runtime/validate_test.go +++ b/pkg/runtime/validate_test.go @@ -33,59 +33,59 @@ func TestValidateInputs(t *testing.T) { { name: "valid filePath", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", }, wantErr: nil, }, { name: "valid dirPath", executor: Executor{ - filePath: "", - dirPath: testDir, - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", + filePath: "", + dirPath: testDir, + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", }, wantErr: nil, }, { name: "valid filePath", executor: Executor{ - filePath: filepath.Join(testDataDir, "testfile"), - dirPath: "", - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", - severity: "high", + filePath: filepath.Join(testDataDir, "testfile"), + dirPath: "", + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", + severity: "high", }, wantErr: nil, }, { name: "valid dirPath", executor: Executor{ - filePath: "", - dirPath: testDir, - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", - severity: "MEDIUM", + filePath: "", + dirPath: testDir, + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", + severity: "MEDIUM", }, wantErr: nil, }, { name: "valid dirPath", executor: Executor{ - filePath: "", - dirPath: testDir, - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v12", - severity: " LOW ", - categories: []string{" identity And ACCESS Management ", "data Protection "}, + filePath: "", + dirPath: testDir, + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v12", + severity: " LOW ", + categories: []string{" identity And ACCESS Management ", "data Protection "}, }, wantErr: nil, }, @@ -130,47 +130,47 @@ func TestValidateInputs(t *testing.T) { { name: "invalid iac type", executor: Executor{ - filePath: "", - dirPath: testDir, - cloudType: []string{"aws"}, - iacType: "notthere", - iacVersion: "v14", + filePath: "", + dirPath: testDir, + policyTypes: []string{"aws"}, + iacType: "notthere", + iacVersion: "v14", }, wantErr: errIacNotSupported, }, { name: "invalid iac version", executor: Executor{ - filePath: "", - dirPath: testDir, - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "notthere", + filePath: "", + dirPath: testDir, + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "notthere", }, wantErr: errIacNotSupported, }, { name: "invalid severity", executor: Executor{ - filePath: "", - dirPath: testDir, - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", - severity: "HGIH", + filePath: "", + dirPath: testDir, + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", + severity: "HGIH", }, wantErr: errSeverityNotSupported, }, { name: "invalid category", executor: Executor{ - filePath: "", - dirPath: testDir, - cloudType: []string{"aws"}, - iacType: "terraform", - iacVersion: "v14", - severity: "HGIH", - categories: []string{"DTA PROTECTIO"}, + filePath: "", + dirPath: testDir, + policyTypes: []string{"aws"}, + iacType: "terraform", + iacVersion: "v14", + severity: "HGIH", + categories: []string{"DTA PROTECTIO"}, }, wantErr: fmt.Errorf(errCategoryNotSupported, []string{"DTA PROTECTIO"}), }, diff --git a/pkg/utils/policy.go b/pkg/utils/policy.go index b59071ed0..c06436173 100644 --- a/pkg/utils/policy.go +++ b/pkg/utils/policy.go @@ -1,3 +1,19 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + package utils import ( @@ -30,3 +46,21 @@ func GetAbsPolicyConfigPaths(policyBasePath, policyRepoPath string) (string, str absolutePolicyRepoPath = filepath.Join(absolutePolicyBasePath, policyRepoPath) return absolutePolicyBasePath, absolutePolicyRepoPath, nil } + +// CheckPolicyType checks if supplied policy type matches desired policy types +func CheckPolicyType(rulePolicyType string, desiredPolicyTypes []string) bool { + normDesiredPolicyTypes := make(map[string]bool, len(desiredPolicyTypes)) + normRulePolicyType := EnsureUpperCaseTrimmed(rulePolicyType) + + for _, desiredPolicyType := range desiredPolicyTypes { + desiredPolicyType = EnsureUpperCaseTrimmed(desiredPolicyType) + normDesiredPolicyTypes[desiredPolicyType] = true + } + + if _, ok := normDesiredPolicyTypes["ALL"]; ok { + return true + } + + _, ok := normDesiredPolicyTypes[normRulePolicyType] + return ok +}