diff --git a/build/Dockerfile b/build/Dockerfile index 3c5d97ebe..c0c16240e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -27,6 +27,10 @@ RUN addgroup --gid 101 terrascan && \ adduser -S --uid 101 --ingroup terrascan terrascan && \ apk add --no-cache git openssh +# create ~/.ssh & ~/bin folder and change owner to terrascan +RUN mkdir -p /home/terrascan/.ssh /home/terrascan/bin && \ + chown -R terrascan:terrascan /home/terrascan + # run as non root user USER terrascan diff --git a/pkg/http-server/remote-repo.go b/pkg/http-server/remote-repo.go index b04d53f47..622a44f3b 100644 --- a/pkg/http-server/remote-repo.go +++ b/pkg/http-server/remote-repo.go @@ -24,7 +24,9 @@ import ( "path/filepath" "strings" + "github.com/accurics/terrascan/pkg/config" "github.com/accurics/terrascan/pkg/downloader" + admissionwebhook "github.com/accurics/terrascan/pkg/k8s/admission-webhook" "github.com/accurics/terrascan/pkg/runtime" "github.com/accurics/terrascan/pkg/utils" "github.com/gorilla/mux" @@ -69,11 +71,12 @@ func (g *APIHandler) scanRemoteRepo(w http.ResponseWriter, r *http.Request) { // scan remote repo s.d = downloader.NewDownloader() var results interface{} + var isAdmissionDenied bool if g.test { - results, err = s.ScanRemoteRepo(iacType, iacVersion, cloudType, []string{"./testdata/testpolicies"}) + results, isAdmissionDenied, err = s.ScanRemoteRepo(iacType, iacVersion, cloudType, []string{"./testdata/testpolicies"}) } else { - results, err = s.ScanRemoteRepo(iacType, iacVersion, cloudType, getPolicyPathFromConfig()) + results, isAdmissionDenied, err = s.ScanRemoteRepo(iacType, iacVersion, cloudType, getPolicyPathFromConfig()) } if err != nil { apiErrorResponse(w, err.Error(), http.StatusBadRequest) @@ -90,17 +93,23 @@ func (g *APIHandler) scanRemoteRepo(w http.ResponseWriter, r *http.Request) { } // return with results + // if result contain violations denied by admission controller return 403 status code + if isAdmissionDenied { + apiResponse(w, string(j), http.StatusForbidden) + return + } apiResponse(w, string(j), http.StatusOK) } // ScanRemoteRepo is the actual method where a remote repo is downloaded and // scanned for violations -func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType []string, policyPath []string) (interface{}, error) { +func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType []string, policyPath []string) (interface{}, bool, error) { // return params var ( - output interface{} - err error + output interface{} + err error + isAdmissionDenied bool ) // temp destination directory to download remote repo @@ -112,7 +121,7 @@ func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType if err != nil { errMsg := fmt.Sprintf("failed to download remote repo. error: '%v'", err) zap.S().Error(errMsg) - return output, err + return output, isAdmissionDenied, err } // create a new runtime executor for scanning the remote repo @@ -120,7 +129,7 @@ func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType "", iacDirPath, policyPath, s.ScanRules, s.SkipRules, s.Categories, s.Severity) if err != nil { zap.S().Error(err) - return output, err + return output, isAdmissionDenied, err } // evaluate policies IaC for violations @@ -128,7 +137,7 @@ func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType if err != nil { errMsg := fmt.Sprintf("failed to scan uploaded file. error: '%v'", err) zap.S().Error(errMsg) - return output, err + return output, isAdmissionDenied, err } if !s.ShowPassed { @@ -139,9 +148,21 @@ func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType if s.ConfigOnly { output = results.ResourceConfig } else { + isAdmissionDenied = hasK8sAdmissionDeniedViolations(results) output = results.Violations } // succesful - return output, nil + return output, isAdmissionDenied, nil +} + +// hasK8sAdmissionDeniedViolations checks if violations have denied by k8s admission controller +func hasK8sAdmissionDeniedViolations(o runtime.Output) bool { + denyRuleMatcher := admissionwebhook.WebhookDenyRuleMatcher{} + for _, v := range o.Violations.ViolationStore.Violations { + if denyRuleMatcher.Match(*v, config.GetK8sAdmissionControl()) { + return true + } + } + return false } diff --git a/pkg/http-server/remote-repo_test.go b/pkg/http-server/remote-repo_test.go index 623e3032e..5bb4fa81d 100644 --- a/pkg/http-server/remote-repo_test.go +++ b/pkg/http-server/remote-repo_test.go @@ -6,10 +6,15 @@ import ( "fmt" "net/http" "net/http/httptest" + "path/filepath" "reflect" "testing" + "github.com/accurics/terrascan/pkg/config" "github.com/accurics/terrascan/pkg/downloader" + "github.com/accurics/terrascan/pkg/policy" + "github.com/accurics/terrascan/pkg/results" + "github.com/accurics/terrascan/pkg/runtime" "github.com/gorilla/mux" ) @@ -69,7 +74,7 @@ func TestScanRemoteRepo(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - gotOutput, gotErr := tt.s.ScanRemoteRepo(tt.iacType, tt.iacVersion, tt.cloudType, []string{}) + gotOutput, _, gotErr := tt.s.ScanRemoteRepo(tt.iacType, tt.iacVersion, tt.cloudType, []string{}) if !reflect.DeepEqual(gotErr, tt.wantErr) { t.Errorf("error got: '%v', want: '%v'", gotErr, tt.wantErr) } @@ -203,3 +208,58 @@ func TestScanRemoteRepoHandler(t *testing.T) { }) } } + +func TestHasK8sAdmissionDeniedViolations(t *testing.T) { + k8sTestData := "k8s_testdata" + configFileWithCategoryDenied := filepath.Join(k8sTestData, "config-deny-category.toml") + + type args struct { + o runtime.Output + } + tests := []struct { + name string + args args + want bool + conigFile string + }{ + { + name: "result with no violations", + args: args{ + o: runtime.Output{ + Violations: policy.EngineOutput{ + ViolationStore: &results.ViolationStore{}, + }, + }, + }, + want: false, + }, + { + name: "result contains denied violations", + args: args{ + o: runtime.Output{ + Violations: policy.EngineOutput{ + ViolationStore: &results.ViolationStore{ + Violations: []*results.Violation{ + { + Category: "Identity and Access Management", + }, + }, + }, + }, + }, + }, + want: true, + conigFile: configFileWithCategoryDenied, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := config.LoadGlobalConfig(tt.conigFile); err != nil { + t.Errorf("error while loading the config file '%s'", tt.conigFile) + } + if got := hasK8sAdmissionDeniedViolations(tt.args.o); got != tt.want { + t.Errorf("hasK8sAdmissionDeniedViolations() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/iac-providers/kubernetes/v1/normalize.go b/pkg/iac-providers/kubernetes/v1/normalize.go index ffc693894..4b1b316af 100644 --- a/pkg/iac-providers/kubernetes/v1/normalize.go +++ b/pkg/iac-providers/kubernetes/v1/normalize.go @@ -159,47 +159,16 @@ func readSkipRulesFromAnnotations(annotations map[string]interface{}, resourceID return nil } - skipRules := make([]output.SkipRule, 0) - if rules, ok := skipRulesFromAnnotations.([]interface{}); ok { - for _, rule := range rules { - if value, ok := rule.(map[string]interface{}); ok { - skipRule := getSkipRuleObject(value) - if skipRule != nil { - skipRules = append(skipRules, *skipRule) - } - } else { - zap.S().Debugf("each rule in %s must be of map type", terrascanSkip) - } - } - } else { - zap.S().Debugf("%s must be an array of {rule: ruleID, comment: reason for skipping}", terrascanSkip) - } - - return skipRules -} - -func getSkipRuleObject(m map[string]interface{}) *output.SkipRule { - var skipRule output.SkipRule - var rule, comment interface{} - var ok bool - - // get rule, if rule not found return nil - if rule, ok = m[terrascanSkipRule]; ok { - if _, ok = rule.(string); ok { - skipRule.Rule = rule.(string) - } else { + if rules, ok := skipRulesFromAnnotations.(string); ok { + skipRules := make([]output.SkipRule, 0) + err := json.Unmarshal([]byte(rules), &skipRules) + if err != nil { + zap.S().Debugf("json string %s cannot be unmarshalled to []output.SkipRules struct schema", rules) return nil } - } else { - return nil - } - - // get comment - if comment, ok = m[terrascanSkipComment]; ok { - if _, ok = comment.(string); ok { - skipRule.Comment = comment.(string) - } + return skipRules } - return &skipRule + zap.S().Debugf("%s must be a string containing an json array like [{rule: ruleID, comment: reason for skipping}]", terrascanSkip) + return nil } diff --git a/pkg/iac-providers/kubernetes/v1/normalize_test.go b/pkg/iac-providers/kubernetes/v1/normalize_test.go index 71b0c5ea5..ec7d49c6c 100644 --- a/pkg/iac-providers/kubernetes/v1/normalize_test.go +++ b/pkg/iac-providers/kubernetes/v1/normalize_test.go @@ -17,6 +17,7 @@ package k8sv1 import ( + "fmt" "reflect" "testing" @@ -55,9 +56,8 @@ kind: Pod metadata: name: myapp-pod annotations: - terrascanSkip: - - rule: accurics.kubernetes.IAM.109 - comment: reason to skip the rule + terrascanSkip: | + [{"rule": "accurics.kubernetes.IAM.109", "comment": "reason to skip the rule"}] spec: containers: - name: myapp-container @@ -68,9 +68,8 @@ kind: CRD metadata: generateName: myapp-pod-prefix- annotations: - terrascanSkip: - - rule: accurics.kubernetes.IAM.109 - comment: reason to skip the rule + terrascanSkip: | + [{"rule": "accurics.kubernetes.IAM.109", "comment": "reason to skip the rule"}] spec: containers: - name: myapp-container @@ -125,10 +124,7 @@ func TestK8sV1ExtractResource(t *testing.T) { Metadata: k8sMetadata{ Name: "myapp-pod", Annotations: map[string]interface{}{ - terrascanSkip: []interface{}{map[string]interface{}{ - "rule": "accurics.kubernetes.IAM.109", - "comment": "reason to skip the rule", - }}, + terrascanSkip: "[{\"rule\": \"accurics.kubernetes.IAM.109\", \"comment\": \"reason to skip the rule\"}]\n", }, }, }, @@ -228,10 +224,7 @@ func TestK8sV1Normalize(t *testing.T) { "kind": "Pod", "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ - terrascanSkip: []interface{}{map[string]interface{}{ - "rule": testRule, - "comment": testComment, - }}, + terrascanSkip: "[{\"rule\": \"accurics.kubernetes.IAM.109\", \"comment\": \"reason to skip the rule\"}]\n", }, "name": "myapp-pod", }, @@ -265,10 +258,7 @@ func TestK8sV1Normalize(t *testing.T) { "kind": "CRD", "metadata": map[string]interface{}{ "annotations": map[string]interface{}{ - terrascanSkip: []interface{}{map[string]interface{}{ - "rule": testRule, - "comment": testComment, - }}, + terrascanSkip: "[{\"rule\": \"accurics.kubernetes.IAM.109\", \"comment\": \"reason to skip the rule\"}]\n", }, "generateName": "myapp-pod-prefix-", }, @@ -341,7 +331,7 @@ func TestReadSkipRulesFromAnnotations(t *testing.T) { terrascanSkip: "test", }, }, - want: []output.SkipRule{}, + want: nil, }, { name: "annotations with invalid SkipRule object", @@ -350,26 +340,22 @@ func TestReadSkipRulesFromAnnotations(t *testing.T) { terrascanSkip: []interface{}{1}, }, }, - want: []output.SkipRule{}, + want: nil, }, { name: "annotations with invalid terrascanSkipRules rule value", args: args{ annotations: map[string]interface{}{ - terrascanSkip: []interface{}{map[string]interface{}{ - terrascanSkipRule: 1, - }}, + terrascanSkip: fmt.Sprintf(`{"%s":%d}`, terrascanSkipRule, 1), }, }, - want: []output.SkipRule{}, + want: nil, }, { name: "annotations with one terrascanSkipRules", args: args{ annotations: map[string]interface{}{ - terrascanSkip: []interface{}{map[string]interface{}{ - terrascanSkipRule: testRuleA, - }}, + terrascanSkip: fmt.Sprintf(`[{"%s":"%s"}]`, terrascanSkipRule, testRuleA), }, }, want: []output.SkipRule{ @@ -382,19 +368,7 @@ func TestReadSkipRulesFromAnnotations(t *testing.T) { name: "annotations with multiple terrascanSkipRules", args: args{ annotations: map[string]interface{}{ - terrascanSkip: []interface{}{ - map[string]interface{}{ - terrascanSkipRule: testRuleA, - terrascanSkipComment: testCommentA, - }, - map[string]interface{}{ - terrascanSkipRule: testRuleB, - terrascanSkipComment: testCommentB, - }, - map[string]interface{}{ - terrascanSkipRule: testRuleC, - terrascanSkipComment: testCommentC, - }}, + terrascanSkip: fmt.Sprintf(`[{"rule":"%s","comment":"%s"}, {"rule":"%s","comment":"%s"}, {"rule":"%s","comment":"%s"}]`, testRuleA, testCommentA, testRuleB, testCommentB, testRuleC, testCommentC), }, }, want: []output.SkipRule{ @@ -416,23 +390,16 @@ func TestReadSkipRulesFromAnnotations(t *testing.T) { name: "annotations with invalid rule key in terrascanSkipRules", args: args{ annotations: map[string]interface{}{ - terrascanSkip: []interface{}{ - map[string]interface{}{ - "skip-rule": testRuleA, - terrascanSkipComment: testCommentA, - }}, + terrascanSkip: fmt.Sprintf(`[{"skip":"%s","comment":"%s"}]`, testRuleA, testCommentA), }, }, - want: []output.SkipRule{}, + want: []output.SkipRule{{Comment: testCommentA}}, }, { name: "annotations with no comment key in terrascanSkipRules", args: args{ annotations: map[string]interface{}{ - terrascanSkip: []interface{}{ - map[string]interface{}{ - terrascanSkipRule: testRuleA, - }}, + terrascanSkip: fmt.Sprintf(`[{"rule":"%s"}]`, testRuleA), }, }, want: []output.SkipRule{testSkipRule}, diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 11519232b..e1e0f5445 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -231,10 +231,10 @@ func (w ValidatingWebhook) getDeniedViolations(violations results.ViolationStore var denyViolations []results.Violation - denyRuleMatcher := webhookDenyRuleMatcher{} + denyRuleMatcher := WebhookDenyRuleMatcher{} for _, violation := range violations.Violations { - if denyRuleMatcher.match(*violation, denyRules) { + if denyRuleMatcher.Match(*violation, denyRules) { denyViolations = append(denyViolations, *violation) } } @@ -326,11 +326,12 @@ func (w ValidatingWebhook) createResponseAdmissionReview( return responseAdmissionReview } -type webhookDenyRuleMatcher struct { +// WebhookDenyRuleMatcher helps in matching violated rules with k8s denied admission control rules +type WebhookDenyRuleMatcher struct { } -// This class should check if one of the violations found is relevant for the specified K8s deny rules -func (g *webhookDenyRuleMatcher) match(violation results.Violation, denyRules config.K8sAdmissionControl) bool { +// Match should check if one of the violations found is relevant for the specified K8s deny rules +func (g *WebhookDenyRuleMatcher) Match(violation results.Violation, denyRules config.K8sAdmissionControl) bool { if denyRules.DeniedSeverity == "" && len(denyRules.Categories) == 0 { return false diff --git a/pkg/k8s/admission-webhook/webhook-deny-rule-matcher_test.go b/pkg/k8s/admission-webhook/webhook-deny-rule-matcher_test.go index c0c58d6a8..84b490ef3 100644 --- a/pkg/k8s/admission-webhook/webhook-deny-rule-matcher_test.go +++ b/pkg/k8s/admission-webhook/webhook-deny-rule-matcher_test.go @@ -95,7 +95,7 @@ func TestDenyRuleMatcher(t *testing.T) { }, } - var denyRuleMatcher = webhookDenyRuleMatcher{} + var denyRuleMatcher = WebhookDenyRuleMatcher{} for _, tt := range table { t.Run(tt.name, func(t *testing.T) { @@ -106,7 +106,7 @@ func TestDenyRuleMatcher(t *testing.T) { Category: tt.ruleCategory, } - result := denyRuleMatcher.match(violation, tt.k8sDenyRules) + result := denyRuleMatcher.Match(violation, tt.k8sDenyRules) if result != tt.expectedResult { t.Errorf("Expected: %v, Got: %v", tt.expectedResult, result) }