From d818b60a5c1b9a5367fa390df3074734a90c410f Mon Sep 17 00:00:00 2001 From: Gaurav Gogia <16029099+gaurav-gogia@users.noreply.github.com> Date: Thu, 31 Mar 2022 15:47:29 +0530 Subject: [PATCH] Feature/endpoint policy download (#1200) * phase 1 * download commercial poilcies phase 2 * phase 3 * unescape characters in json metadata * add test data * add test case for conversion * fix variable name * status code check add * add response status code * add comments for exported functions * make empty docker folder to satisfy dir structure * change to use 'environment' keyword * env keyword, %w to wrap errors * add file hader * wrap errors in %w * rm tabsapce const * addressing review comments * use bytes.equal * add method for getType, constructor for newPolicy * changes: 1. save IO operations, avoid overwriting rego code file 2. wrap errors wherever required * invalid policy test cases * add table error cases structure * minor fix * docker error return nil check * no error if response code 404 Co-authored-by: Gaurav Gogia --- pkg/config/global.go | 28 ++- pkg/config/types.go | 4 + pkg/initialize/run.go | 183 +++++++++++++++-- pkg/initialize/run_test.go | 185 ++++++++++++++++++ pkg/initialize/test_data/AC_AWS_0452.json | 17 ++ pkg/initialize/test_data/AC_AZURE_0330.json | 18 ++ pkg/initialize/test_data/AC_GCP_0095.json | 19 ++ ...wsNoRetentionPolicyCloudwatchLogGroup.rego | 19 ++ .../ensureProperSettings_25082021.rego | 18 ++ .../test_data/invalid_policies.json | 17 ++ .../test_data/invalid_ruleArg_policies.json | 68 +++++++ .../invalid_ruleArg_type_policies.json | 68 +++++++ ...workPortExposedToInternetGCP_25082021.rego | 34 ++++ pkg/initialize/test_data/policies.json | 68 +++++++ pkg/initialize/types.go | 94 +++++++++ 15 files changed, 822 insertions(+), 18 deletions(-) create mode 100644 pkg/initialize/run_test.go create mode 100644 pkg/initialize/test_data/AC_AWS_0452.json create mode 100644 pkg/initialize/test_data/AC_AZURE_0330.json create mode 100644 pkg/initialize/test_data/AC_GCP_0095.json create mode 100644 pkg/initialize/test_data/awsNoRetentionPolicyCloudwatchLogGroup.rego create mode 100644 pkg/initialize/test_data/ensureProperSettings_25082021.rego create mode 100644 pkg/initialize/test_data/invalid_policies.json create mode 100644 pkg/initialize/test_data/invalid_ruleArg_policies.json create mode 100644 pkg/initialize/test_data/invalid_ruleArg_type_policies.json create mode 100644 pkg/initialize/test_data/networkPortExposedToInternetGCP_25082021.rego create mode 100644 pkg/initialize/test_data/policies.json create mode 100644 pkg/initialize/types.go diff --git a/pkg/config/global.go b/pkg/config/global.go index f82f75b69..0e82cc146 100644 --- a/pkg/config/global.go +++ b/pkg/config/global.go @@ -18,14 +18,16 @@ package config import ( "path/filepath" + "strings" "github.com/accurics/terrascan/pkg/utils" "go.uber.org/zap" ) const ( - defaultPolicyRepoURL = "https://github.com/accurics/terrascan.git" - defaultPolicyBranch = "master" + defaultPolicyRepoURL = "https://github.com/accurics/terrascan.git" + defaultPolicyBranch = "master" + defaultPolicyEnvironment = "https://cloud.tenable.com" ) // ConfigEnvvarName env variable @@ -92,6 +94,12 @@ func LoadGlobalConfig(configFile string) error { if len(configReader.getPolicyConfig().Branch) > 0 { global.Policy.Branch = configReader.getPolicyConfig().Branch } + if len(configReader.getPolicyConfig().Environment) > 0 { + global.Policy.Environment = configReader.getPolicyConfig().Environment + } + if len(configReader.getPolicyConfig().AccessToken) > 0 { + global.Policy.AccessToken = configReader.getPolicyConfig().AccessToken + } if len(configReader.getRules().ScanRules) > 0 { global.Rules.ScanRules = configReader.getRules().ScanRules @@ -152,6 +160,22 @@ func GetPolicyBranch() string { return global.Policy.Branch } +// GetPolicyEnvironment returns the configured policy environment url +func GetPolicyEnvironment() string { + if global == nil { + return defaultPolicyEnvironment + } + return strings.TrimRight(global.Policy.Environment, "/") +} + +// GetPolicyAccessToken returns the configured policy access token +func GetPolicyAccessToken() string { + if global == nil { + return "" + } + return global.Policy.AccessToken +} + // GetScanRules returns the configured scan rules func GetScanRules() []string { if global == nil { diff --git a/pkg/config/types.go b/pkg/config/types.go index 954a0dcc8..510532abd 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -49,6 +49,10 @@ type Policy struct { // policy git url and branch RepoURL string `toml:"repo_url,omitempty" yaml:"repo_url,omitempty"` Branch string `toml:"branch,omitempty" yaml:"branch,omitempty"` + + // policy environment and access token + Environment string `toml:"environment,omitempty" yaml:"environment,omitempty"` + AccessToken string `toml:"access_token,omitempty" yaml:"access_token,omitempty"` } // Notifier represent a single notification in the terrascan config file diff --git a/pkg/initialize/run.go b/pkg/initialize/run.go index 3f9d197d4..b5c7e0a11 100644 --- a/pkg/initialize/run.go +++ b/pkg/initialize/run.go @@ -17,9 +17,14 @@ package initialize import ( + "bytes" + "encoding/json" "fmt" + "io/fs" + "io/ioutil" "net/http" "os" + "path/filepath" "github.com/accurics/terrascan/pkg/config" "go.uber.org/zap" @@ -33,6 +38,7 @@ var ( ) const terrascanReadmeURL string = "https://raw.githubusercontent.com/accurics/terrascan/master/README.md" +const filePermissionBits fs.FileMode = 0755 // Run initializes terrascan if not done already func Run(isNonInitCmd bool) error { @@ -45,10 +51,6 @@ func Run(isNonInitCmd bool) error { zap.S().Debug("initializing terrascan") - if !connected(terrascanReadmeURL) { - return errNoConnection - } - // download policies if err := DownloadPolicies(); err != nil { return err @@ -60,20 +62,169 @@ func Run(isNonInitCmd bool) error { // DownloadPolicies clones the policies to a local folder func DownloadPolicies() error { - + accessToken := config.GetPolicyAccessToken() policyBasePath := config.GetPolicyBasePath() - repoURL := config.GetPolicyRepoURL() - branch := config.GetPolicyBranch() zap.S().Debug("downloading policies") - zap.S().Debugf("base directory path : %s", policyBasePath) - zap.S().Debugf("policy directory path : %s", config.GetPolicyRepoPath()) - zap.S().Debugf("policy repo url : %s", repoURL) - zap.S().Debugf("policy repo git branch : %s", branch) - os.RemoveAll(policyBasePath) + err := os.RemoveAll(policyBasePath) + if err != nil { + return fmt.Errorf("unable to delete base folder. error: '%w'", err) + } + + if accessToken == "" { + return downloadDefaultPolicies(policyBasePath) + } + + return dowloadEnvironmentPolicies(policyBasePath, accessToken) +} + +func dowloadEnvironmentPolicies(policyBasePath, accessToken string) error { + err := ensureDir(policyBasePath) + if err != nil { + return err + } + + policyRepoPath := config.GetPolicyRepoPath() + err = os.MkdirAll(policyRepoPath, filePermissionBits) + if err != nil { + return fmt.Errorf("unable to prepare directories representing policyRepoPath. err: '%w', policyRepoPath: '%s'", err, policyRepoPath) + } + + const apiPath = "/v1/api/app/rules?default=true" + environment := config.GetPolicyEnvironment() + zap.S().Debugf("policy environment : %s", environment) + zap.S().Debugf("downloading environment policies in %s", policyBasePath) + + var client http.Client + + req, err := http.NewRequest(http.MethodGet, environment+apiPath, nil) + if err != nil { + return fmt.Errorf("error constructing request object. error: '%w'", err) + } + req.Header.Add("Authorization", "Bearer "+accessToken) + + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("error downloading environment policies. error: '%w'", err) + } + if res.StatusCode != 200 { + return fmt.Errorf("error downloading environment policies, response status code: '%d'", res.StatusCode) + } + + policies, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("error reading api call response for environment policies. error: '%w'", err) + } + + err = convertEnvironmentPolicies(policies, policyRepoPath) + if err != nil { + return err + } + + // creating empty docker folder to satisfy folder structure dep + dockerPath := filepath.Join(policyRepoPath, "docker") + err = os.Mkdir(dockerPath, filePermissionBits) + if err != nil { + return fmt.Errorf("unable to create empty docker dir. error: '%w'", err) + } + + return nil +} + +func convertEnvironmentPolicies(policies []byte, policyRepoPath string) error { + var ruleMetadataList []environmentPolicyMetadata + + err := json.Unmarshal(policies, &ruleMetadataList) + if err != nil { + return fmt.Errorf("failed to unmarshal policies into structure. error: '%w'", err) + } + + for _, ruleMetadata := range ruleMetadataList { + policy, err := newPolicy(ruleMetadata) + if err != nil { + return err + } + + err = saveEnvironmentPolicies(policy, policyRepoPath) + if err != nil { + return err + } + } + + return nil +} + +func saveEnvironmentPolicies(policy environmentPolicy, policyRepoPath string) error { + policy.policyMetadata.PolicyType = policy.getType() + cspDir := filepath.Join(policyRepoPath, policy.policyMetadata.PolicyType) + err := ensureDir(cspDir) + if err != nil { + return err + } + + resourceDir := filepath.Join(cspDir, policy.resourceType) + err = ensureDir(resourceDir) + if err != nil { + return err + } + + var buffer bytes.Buffer + encoder := json.NewEncoder(&buffer) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + err = encoder.Encode(policy.policyMetadata) + if err != nil { + return fmt.Errorf("could not marshal json object into byte array. error: '%w'", err) + } + + metadata := buffer.Bytes() + metadata = bytes.TrimRight(metadata, "\n") + metaDataPath := filepath.Join(resourceDir, policy.metadataFileName) + err = ioutil.WriteFile(metaDataPath, metadata, os.ModePerm) + if err != nil { + return fmt.Errorf("could not write rule metadata file on disk. error: '%w'", err) + } + + regoPath := filepath.Join(resourceDir, policy.policyMetadata.File) + + if _, err := os.Stat(regoPath); os.IsExist(err) { + zap.S().Debug("rego code file %s exists, skipping", regoPath) + return nil + } + + err = ioutil.WriteFile(regoPath, []byte(policy.regoTemplate), filePermissionBits) + if err != nil { + return fmt.Errorf("could not write rego code file on disk. error: '%w'", err) + } + + return nil +} + +func ensureDir(path string) error { + _, err := os.Stat(path) + if os.IsNotExist(err) { + err = os.Mkdir(path, filePermissionBits) + if err != nil { + return fmt.Errorf("unable to create requested directory error: '%w' for path: '%s'", err, path) + } + } + return nil +} + +func downloadDefaultPolicies(policyBasePath string) error { + if !connected(terrascanReadmeURL) { + return errNoConnection + } + + repoURL := config.GetPolicyRepoURL() + branch := config.GetPolicyBranch() + + zap.S().Debugf("policy directory path : %s", repoURL) + zap.S().Debugf("policy repo url : %s", repoURL) + zap.S().Debugf("policy repo git branch : %s", branch) zap.S().Debugf("cloning terrascan repo at %s", policyBasePath) // clone the repo @@ -82,13 +233,13 @@ func DownloadPolicies() error { }) if err != nil { - return fmt.Errorf("failed to download policies. error: '%v'", err) + return fmt.Errorf("failed to download policies. error: '%w'", err) } // create working tree w, err := r.Worktree() if err != nil { - return fmt.Errorf("failed to create working tree. error: '%v'", err) + return fmt.Errorf("failed to create working tree. error: '%w'", err) } // fetch references @@ -96,7 +247,7 @@ func DownloadPolicies() error { RefSpecs: []gitConfig.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}, }) if err != nil { - return fmt.Errorf("failed to fetch references from git repo. error: '%v'", err) + return fmt.Errorf("failed to fetch references from git repo. error: '%w'", err) } // checkout policies branch @@ -105,7 +256,7 @@ func DownloadPolicies() error { Force: true, }) if err != nil { - return fmt.Errorf("failed to checkout git branch '%v'. error: '%v'", branch, err) + return fmt.Errorf("failed to checkout git branch '%s'. error: '%w'", branch, err) } return nil diff --git a/pkg/initialize/run_test.go b/pkg/initialize/run_test.go new file mode 100644 index 000000000..3c152bb43 --- /dev/null +++ b/pkg/initialize/run_test.go @@ -0,0 +1,185 @@ +/* + Copyright (C) 2022 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 initialize + +import ( + "bytes" + "errors" + "fmt" + "io/fs" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" +) + +const TESTDIR = "test_data" + +func TestGetCommercialPolicy(t *testing.T) { + table := []struct { + name string + configFile string + wantErr error + }{ + { + name: "invalid policy json", + configFile: filepath.Join(TESTDIR, "invalid_policies.json"), + wantErr: errors.New("failed to unmarshal policies into structure"), + }, + { + name: "invalid ruleArgument data type", + configFile: filepath.Join(TESTDIR, "invalid_ruleArg_type_policies.json"), + wantErr: errors.New("incorrect rule argument type, must be a string"), + }, + { + name: "invalid ruleArgument format", + configFile: filepath.Join(TESTDIR, "invalid_ruleArg_policies.json"), + wantErr: errors.New("error occurred while unmarshaling rule arguments into map[string]interface{}"), + }, + { + name: "valid policy json", + configFile: filepath.Join(TESTDIR, "policies.json"), + wantErr: nil, + }, + } + + tempDir, err := os.MkdirTemp("", "test_policies") + if err != nil { + t.Errorf("unable to create temporary dir for testing") + } + defer os.RemoveAll(tempDir) + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + policies, err := ioutil.ReadFile(tt.configFile) + if err != nil { + t.Errorf("unable to read test file") + } + + err = convertEnvironmentPolicies(policies, tempDir) + if err == nil { + validPoliciesTest(t, tempDir) + return + } + + if !strings.HasPrefix(err.Error(), tt.wantErr.Error()) { + t.Errorf("convertEnvironmentPolicies() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +func validPoliciesTest(t *testing.T, tempDir string) { + expectedCspMap := map[string]string{ + "aws": "aws_cloudwatch_log_group", + "azure": "azurerm_security_center_setting", + "gcp": "google_compute_firewall", + } + + providers, err := os.ReadDir(tempDir) + if err != nil { + t.Errorf("unable to read temp dir: '%v'", err) + } + if len(providers) != 3 { + t.Errorf("expected length 3; got: '%d'", len(providers)) + } + + for _, provider := range providers { + cspdir := filepath.Join(tempDir, provider.Name()) + rscs, err := os.ReadDir(cspdir) + if err != nil { + t.Errorf("unable to read csp dir: '%v'", err) + } + if len(rscs) != 1 { + t.Errorf("expected length 1; got '%d'", len(rscs)) + } + + val, ok := expectedCspMap[provider.Name()] + if !ok { + t.Errorf("unable to find expected csp") + + } + if rscs[0].Name() != val { + t.Errorf("unable to find expected resource type") + } + + rscdir := filepath.Join(cspdir, rscs[0].Name()) + files, err := os.ReadDir(rscdir) + if err != nil { + t.Errorf("unable to read resource type dir: '%v'", err) + } + + err = verifyFiles(files, rscdir) + if err != nil { + t.Error(err) + } + } +} + +func verifyFiles(files []fs.DirEntry, rscdir string) error { + expectedMetaMap := map[string]bool{ + "AC_AWS_0452.json": true, + "AC_AZURE_0330.json": true, + "AC_GCP_0095.json": true, + } + + expectedRegoMap := map[string]bool{ + "awsNoRetentionPolicyCloudwatchLogGroup.rego": true, + "ensureProperSettings_25082021.rego": true, + "networkPortExposedToInternetGCP_25082021.rego": true, + } + + if len(files) != 2 { + return fmt.Errorf("expected length 2; got: '%d'", len(files)) + } + + for _, file := range files { + if strings.HasSuffix(file.Name(), ".json") { + _, ok := expectedMetaMap[file.Name()] + if !ok { + return fmt.Errorf("expected metadata file not found") + } + } + + if strings.HasSuffix(file.Name(), ".rego") { + _, ok := expectedRegoMap[file.Name()] + if !ok { + return fmt.Errorf("expected rego file not found") + } + } + + testpath := filepath.Join(TESTDIR, file.Name()) + convpath := filepath.Join(rscdir, file.Name()) + + testbytes, err := ioutil.ReadFile(testpath) + if err != nil { + return fmt.Errorf("unable to read test file: '%s', err: '%w'", testpath, err) + } + + databytes, err := ioutil.ReadFile(convpath) + if err != nil { + return fmt.Errorf("unable to read converted file: '%s', err: '%w'", convpath, err) + } + + if !bytes.Equal(testbytes, databytes) { + return fmt.Errorf("test file and converted file do not match") + } + } + + return nil +} diff --git a/pkg/initialize/test_data/AC_AWS_0452.json b/pkg/initialize/test_data/AC_AWS_0452.json new file mode 100644 index 000000000..b89cc690d --- /dev/null +++ b/pkg/initialize/test_data/AC_AWS_0452.json @@ -0,0 +1,17 @@ +{ + "name": "awsNoRetentionPolicyCloudwatchLogGroup", + "file": "awsNoRetentionPolicyCloudwatchLogGroup.rego", + "policy_type": "aws", + "resource_type": "aws_cloudwatch_log_group", + "template_args": { + "name": "awsNoRetentionPolicyCloudwatchLogGroup", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Ensure log retention policy is set for AWS CloudWatch Log Group", + "reference_id": "AC_AWS_0452", + "category": "Security Best Practices", + "version": 2, + "id": "AC_AWS_0452" +} \ No newline at end of file diff --git a/pkg/initialize/test_data/AC_AZURE_0330.json b/pkg/initialize/test_data/AC_AZURE_0330.json new file mode 100644 index 000000000..747ecb17d --- /dev/null +++ b/pkg/initialize/test_data/AC_AZURE_0330.json @@ -0,0 +1,18 @@ +{ + "name": "ensureMcasInArm_25082021", + "file": "ensureProperSettings_25082021.rego", + "policy_type": "azure", + "resource_type": "azurerm_security_center_setting", + "template_args": { + "name": "ensureMcasInArm", + "prefix": "", + "suffix": "_25082021", + "value": "MCAS" + }, + "severity": "MEDIUM", + "description": "Ensure that Microsoft Cloud App Security (MCAS) integration is selected in Azure Security Center Setting", + "reference_id": "AC_AZURE_0330", + "category": "Compliance Validation", + "version": 2, + "id": "AC_AZURE_0330" +} \ No newline at end of file diff --git a/pkg/initialize/test_data/AC_GCP_0095.json b/pkg/initialize/test_data/AC_GCP_0095.json new file mode 100644 index 000000000..ae7a0c5a6 --- /dev/null +++ b/pkg/initialize/test_data/AC_GCP_0095.json @@ -0,0 +1,19 @@ +{ + "name": "networkPort139ExposedToInternetGCP_25082021", + "file": "networkPortExposedToInternetGCP_25082021.rego", + "policy_type": "gcp", + "resource_type": "google_compute_firewall", + "template_args": { + "name": "networkPort139ExposedToInternetGCP", + "portNumber": 139, + "prefix": "", + "protocol": "tcp", + "suffix": "_25082021" + }, + "severity": "HIGH", + "description": "Ensure NetBios Session Service (TCP:139) is not exposed to entire internet for Google Compute Firewall", + "reference_id": "AC_GCP_0095", + "category": "Infrastructure Security", + "version": 2, + "id": "AC_GCP_0095" +} \ No newline at end of file diff --git a/pkg/initialize/test_data/awsNoRetentionPolicyCloudwatchLogGroup.rego b/pkg/initialize/test_data/awsNoRetentionPolicyCloudwatchLogGroup.rego new file mode 100644 index 000000000..54a95caa2 --- /dev/null +++ b/pkg/initialize/test_data/awsNoRetentionPolicyCloudwatchLogGroup.rego @@ -0,0 +1,19 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[retVal] { + cw_log_group := input.aws_cloudwatch_log_group[_] + cw_log_group.config.retention_in_days == 0 + + traverse := "retention_in_days" + + retVal := { + "Id": cw_log_group.id, + "ReplaceType": "edit", + "CodeType": "attribute", + "Traverse": traverse, + "Attribute": traverse, + "AttributeDataType": "int", + "Expected": 120, + "Actual": cw_log_group.config.retention_in_days + } +} \ No newline at end of file diff --git a/pkg/initialize/test_data/ensureProperSettings_25082021.rego b/pkg/initialize/test_data/ensureProperSettings_25082021.rego new file mode 100644 index 000000000..b4e4d117e --- /dev/null +++ b/pkg/initialize/test_data/ensureProperSettings_25082021.rego @@ -0,0 +1,18 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[retval] { + security_center_settings := input.azurerm_security_center_setting[_] + upper(security_center_settings.config.setting_name) != "{{.value}}" + security_center_settings.config.enabled != true + + retval := { + "Id": security_center_settings.id, + "ReplaceType": "edit", + "CodeType": "attribute", + "Traverse": "enabled", + "Attribute": "enabled", + "AttributeDataType": "bool", + "Expected": true, + "Actual": security_center_settings.config.enabled + } +} \ No newline at end of file diff --git a/pkg/initialize/test_data/invalid_policies.json b/pkg/initialize/test_data/invalid_policies.json new file mode 100644 index 000000000..a501d5583 --- /dev/null +++ b/pkg/initialize/test_data/invalid_policies.json @@ -0,0 +1,17 @@ +{ + "id": "058ef84d-9ad0-4ea1-83eb-9fd2f3e8f2d8", + "ruleTemplateId": "86e88b41-a767-4b9f-9bbc-33a86bd9f6b7", + "ruleName": "ensureMcasInArm_25082021", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retval] {\n security_center_settings := input.azurerm_security_center_setting[_]\n upper(security_center_settings.config.setting_name) != \"{{.value}}\"\n security_center_settings.config.enabled != true\n\n retval := {\n \"Id\": security_center_settings.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": \"enabled\",\n \"Attribute\": \"enabled\",\n \"AttributeDataType\": \"bool\",\n \"Expected\": true,\n \"Actual\": security_center_settings.config.enabled\n }\n}", + "ruleTemplateName": "ensureProperSettings_25082021", + "ruleArgument": "{\"name\":\"ensureMcasInArm\",\"prefix\":\"\",\"suffix\":\"_25082021\",\"value\":\"MCAS\"}", + "severity": "MEDIUM", + "vulnerability": "Disabled MCAS in Azure Security Center Setting goes against compliance.", + "remediation": "Ensure MCAS setting is enabled in Azure Security Center Setting.", + "engineType": "terraform", + "provider": "azure", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure that Microsoft Cloud App Security (MCAS) integration is selected in Azure Security Center Setting", + "category": "Compliance Validation", + "poli": "CIS-1.3:2.10" +} \ No newline at end of file diff --git a/pkg/initialize/test_data/invalid_ruleArg_policies.json b/pkg/initialize/test_data/invalid_ruleArg_policies.json new file mode 100644 index 000000000..54ccfbb4e --- /dev/null +++ b/pkg/initialize/test_data/invalid_ruleArg_policies.json @@ -0,0 +1,68 @@ +[ + { + "id": "003ec732-5bd5-4312-9c0a-533386288937", + "ruleTemplateId": "c75fa0c8-335e-4e16-b911-7ee8e3bb259d", + "ruleName": "networkPort139ExposedToInternetGCP_25082021", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retVal] {\n rule := input.google_compute_firewall[_]\n config := rule.config\n config.direction == \"INGRESS\"\n config.source_ranges[_] == \"0.0.0.0/0\"\n fire_rule := config.allow[_]\n fire_rule.protocol == \"{{.protocol}}\"\n fire_rule.ports[_] == \"{{.portNumber}}\"\n\n expected := [ item | item := validate_source(config.source_ranges[_]) ]\n\ttraverse := \"source_ranges\"\n\n retVal := {\n \"Id\": rule.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": traverse,\n \"Attribute\": traverse,\n \"AttributeDataType\": \"list\",\n \"Expected\": expected,\n \"Actual\": config.source_ranges\n }\n}\n\nvalidate_source(source) = value {\n\tsource == \"0.0.0.0/0\"\n value := \"\"\n}\nvalidate_source(source) = value {\n\tsource != \"0.0.0.0/0\"\n value := source\n}", + "ruleTemplateName": "networkPortExposedToInternetGCP_25082021", + "ruleArgument": "{\"name\":\"networkPort139ExposedToInternetGCP\",\"portNumber\":139,\"prefix\":\"\",\"protocol\":\"tcp\",\"suffix\":\"_25082021\"}", + "severity": "HIGH", + "vulnerability": "NetBios Session Service (TCP:139) is exposed to entire internet for Google Compute Firewall.", + "remediation": "Limit the access scope for NetBios Session Service to only allow access in internal networks and limited scope. If public interface exists, remove it and limit the access scope within the VNET only to applications or instances that requires access.", + "engineType": "terraform", + "provider": "gcp", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure NetBios Session Service (TCP:139) is not exposed to entire internet for Google Compute Firewall", + "category": "Infrastructure Security", + "policyRelevance": "NIST-800-53:SC-7(11)&SC-7(21) ISO-27001:A.9.1.2&A.13.1.1 SOC2:CC6.6", + "ruleReferenceId": "AC_GCP_0095", + "policy": "Accurics Security Best Practices for GCP v2", + "version": 2, + "custom": false, + "resourceType": "google_compute_firewall" + }, + { + "id": "04eac6e0-c02a-41d2-8fae-3a0ef74d55b0", + "ruleTemplateId": "473369ba-4e7d-43e1-b66c-73a5f92e8b87", + "ruleName": "awsNoRetentionPolicyCloudwatchLogGroup", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retVal] {\n cw_log_group := input.aws_cloudwatch_log_group[_]\n cw_log_group.config.retention_in_days == 0\n\n traverse := \"retention_in_days\"\n\n retVal := {\n \"Id\": cw_log_group.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": traverse,\n \"Attribute\": traverse,\n \"AttributeDataType\": \"int\",\n \"Expected\": 120,\n \"Actual\": cw_log_group.config.retention_in_days\n }\n}", + "ruleTemplateName": "awsNoRetentionPolicyCloudwatchLogGroup", + "ruleArgument": "{\"name\":\"awsNoRetentionPolicyCloudwatchLogGroup\",\"prefix\":\"\",\"suffix\":\"\"}", + "severity": "MEDIUM", + "vulnerability": "AWS CloudWatch Log Group does not have a retention policy set in order to establish how long log events are kept in AWS CloudWatch Logs.", + "remediation": "AWS CloudWatch Log Group can be set with a log retention policy using AWS Console.\n In AWS Console - \n 1. Sign in to AWS Console and go to the CloudWatch console.\n 2. Select Log Groups in the navigation pane.\n 3. Select the log group to update.\n 4. Select 'Edit retention' and change the log retention value to 120 days.", + "engineType": "terraform", + "provider": "aws", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure log retention policy is set for AWS CloudWatch Log Group", + "category": "Security Best Practices", + "policyRelevance": "", + "ruleReferenceId": "AC_AWS_0452", + "policy": "Accurics Security Best Practices for AWS v2", + "version": 2, + "custom": false, + "resourceType": "aws_cloudwatch_log_group" + }, + { + "id": "058ef84d-9ad0-4ea1-83eb-9fd2f3e8f2d8", + "ruleTemplateId": "86e88b41-a767-4b9f-9bbc-33a86bd9f6b7", + "ruleName": "ensureMcasInArm_25082021", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retval] {\n security_center_settings := input.azurerm_security_center_setting[_]\n upper(security_center_settings.config.setting_name) != \"{{.value}}\"\n security_center_settings.config.enabled != true\n\n retval := {\n \"Id\": security_center_settings.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": \"enabled\",\n \"Attribute\": \"enabled\",\n \"AttributeDataType\": \"bool\",\n \"Expected\": true,\n \"Actual\": security_center_settings.config.enabled\n }\n}", + "ruleTemplateName": "ensureProperSettings_25082021", + "ruleArgument": "\"name\":\"ensureMcasInArm\"\"prefix\"\"\",[]\"suffix\":\"_25082021\",\"value\":\"MCAS\"}", + "severity": "MEDIUM", + "vulnerability": "Disabled MCAS in Azure Security Center Setting goes against compliance.", + "remediation": "Ensure MCAS setting is enabled in Azure Security Center Setting.", + "engineType": "terraform", + "provider": "azure", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure that Microsoft Cloud App Security (MCAS) integration is selected in Azure Security Center Setting", + "category": "Compliance Validation", + "policyRelevance": "CIS-1.3:2.10", + "ruleReferenceId": "AC_AZURE_0330", + "policy": "Accurics Security Best Practices for Azure v2", + "version": 2, + "custom": false, + "resourceType": "azurerm_security_center_setting" + } +] \ No newline at end of file diff --git a/pkg/initialize/test_data/invalid_ruleArg_type_policies.json b/pkg/initialize/test_data/invalid_ruleArg_type_policies.json new file mode 100644 index 000000000..c53c56ed0 --- /dev/null +++ b/pkg/initialize/test_data/invalid_ruleArg_type_policies.json @@ -0,0 +1,68 @@ +[ + { + "id": "003ec732-5bd5-4312-9c0a-533386288937", + "ruleTemplateId": "c75fa0c8-335e-4e16-b911-7ee8e3bb259d", + "ruleName": "networkPort139ExposedToInternetGCP_25082021", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retVal] {\n rule := input.google_compute_firewall[_]\n config := rule.config\n config.direction == \"INGRESS\"\n config.source_ranges[_] == \"0.0.0.0/0\"\n fire_rule := config.allow[_]\n fire_rule.protocol == \"{{.protocol}}\"\n fire_rule.ports[_] == \"{{.portNumber}}\"\n\n expected := [ item | item := validate_source(config.source_ranges[_]) ]\n\ttraverse := \"source_ranges\"\n\n retVal := {\n \"Id\": rule.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": traverse,\n \"Attribute\": traverse,\n \"AttributeDataType\": \"list\",\n \"Expected\": expected,\n \"Actual\": config.source_ranges\n }\n}\n\nvalidate_source(source) = value {\n\tsource == \"0.0.0.0/0\"\n value := \"\"\n}\nvalidate_source(source) = value {\n\tsource != \"0.0.0.0/0\"\n value := source\n}", + "ruleTemplateName": "networkPortExposedToInternetGCP_25082021", + "ruleArgument": "{\"name\":\"networkPort139ExposedToInternetGCP\",\"portNumber\":139,\"prefix\":\"\",\"protocol\":\"tcp\",\"suffix\":\"_25082021\"}", + "severity": "HIGH", + "vulnerability": "NetBios Session Service (TCP:139) is exposed to entire internet for Google Compute Firewall.", + "remediation": "Limit the access scope for NetBios Session Service to only allow access in internal networks and limited scope. If public interface exists, remove it and limit the access scope within the VNET only to applications or instances that requires access.", + "engineType": "terraform", + "provider": "gcp", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure NetBios Session Service (TCP:139) is not exposed to entire internet for Google Compute Firewall", + "category": "Infrastructure Security", + "policyRelevance": "NIST-800-53:SC-7(11)&SC-7(21) ISO-27001:A.9.1.2&A.13.1.1 SOC2:CC6.6", + "ruleReferenceId": "AC_GCP_0095", + "policy": "Accurics Security Best Practices for GCP v2", + "version": 2, + "custom": false, + "resourceType": "google_compute_firewall" + }, + { + "id": "04eac6e0-c02a-41d2-8fae-3a0ef74d55b0", + "ruleTemplateId": "473369ba-4e7d-43e1-b66c-73a5f92e8b87", + "ruleName": "awsNoRetentionPolicyCloudwatchLogGroup", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retVal] {\n cw_log_group := input.aws_cloudwatch_log_group[_]\n cw_log_group.config.retention_in_days == 0\n\n traverse := \"retention_in_days\"\n\n retVal := {\n \"Id\": cw_log_group.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": traverse,\n \"Attribute\": traverse,\n \"AttributeDataType\": \"int\",\n \"Expected\": 120,\n \"Actual\": cw_log_group.config.retention_in_days\n }\n}", + "ruleTemplateName": "awsNoRetentionPolicyCloudwatchLogGroup", + "ruleArgument": "{\"name\":\"awsNoRetentionPolicyCloudwatchLogGroup\",\"prefix\":\"\",\"suffix\":\"\"}", + "severity": "MEDIUM", + "vulnerability": "AWS CloudWatch Log Group does not have a retention policy set in order to establish how long log events are kept in AWS CloudWatch Logs.", + "remediation": "AWS CloudWatch Log Group can be set with a log retention policy using AWS Console.\n In AWS Console - \n 1. Sign in to AWS Console and go to the CloudWatch console.\n 2. Select Log Groups in the navigation pane.\n 3. Select the log group to update.\n 4. Select 'Edit retention' and change the log retention value to 120 days.", + "engineType": "terraform", + "provider": "aws", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure log retention policy is set for AWS CloudWatch Log Group", + "category": "Security Best Practices", + "policyRelevance": "", + "ruleReferenceId": "AC_AWS_0452", + "policy": "Accurics Security Best Practices for AWS v2", + "version": 2, + "custom": false, + "resourceType": "aws_cloudwatch_log_group" + }, + { + "id": "058ef84d-9ad0-4ea1-83eb-9fd2f3e8f2d8", + "ruleTemplateId": "86e88b41-a767-4b9f-9bbc-33a86bd9f6b7", + "ruleName": "ensureMcasInArm_25082021", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retval] {\n security_center_settings := input.azurerm_security_center_setting[_]\n upper(security_center_settings.config.setting_name) != \"{{.value}}\"\n security_center_settings.config.enabled != true\n\n retval := {\n \"Id\": security_center_settings.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": \"enabled\",\n \"Attribute\": \"enabled\",\n \"AttributeDataType\": \"bool\",\n \"Expected\": true,\n \"Actual\": security_center_settings.config.enabled\n }\n}", + "ruleTemplateName": "ensureProperSettings_25082021", + "ruleArgument": false, + "severity": "MEDIUM", + "vulnerability": "Disabled MCAS in Azure Security Center Setting goes against compliance.", + "remediation": "Ensure MCAS setting is enabled in Azure Security Center Setting.", + "engineType": "terraform", + "provider": "azure", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure that Microsoft Cloud App Security (MCAS) integration is selected in Azure Security Center Setting", + "category": "Compliance Validation", + "policyRelevance": "CIS-1.3:2.10", + "ruleReferenceId": "AC_AZURE_0330", + "policy": "Accurics Security Best Practices for Azure v2", + "version": 2, + "custom": false, + "resourceType": "azurerm_security_center_setting" + } +] \ No newline at end of file diff --git a/pkg/initialize/test_data/networkPortExposedToInternetGCP_25082021.rego b/pkg/initialize/test_data/networkPortExposedToInternetGCP_25082021.rego new file mode 100644 index 000000000..0a0772e9d --- /dev/null +++ b/pkg/initialize/test_data/networkPortExposedToInternetGCP_25082021.rego @@ -0,0 +1,34 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[retVal] { + rule := input.google_compute_firewall[_] + config := rule.config + config.direction == "INGRESS" + config.source_ranges[_] == "0.0.0.0/0" + fire_rule := config.allow[_] + fire_rule.protocol == "{{.protocol}}" + fire_rule.ports[_] == "{{.portNumber}}" + + expected := [ item | item := validate_source(config.source_ranges[_]) ] + traverse := "source_ranges" + + retVal := { + "Id": rule.id, + "ReplaceType": "edit", + "CodeType": "attribute", + "Traverse": traverse, + "Attribute": traverse, + "AttributeDataType": "list", + "Expected": expected, + "Actual": config.source_ranges + } +} + +validate_source(source) = value { + source == "0.0.0.0/0" + value := "" +} +validate_source(source) = value { + source != "0.0.0.0/0" + value := source +} \ No newline at end of file diff --git a/pkg/initialize/test_data/policies.json b/pkg/initialize/test_data/policies.json new file mode 100644 index 000000000..2b995d0a1 --- /dev/null +++ b/pkg/initialize/test_data/policies.json @@ -0,0 +1,68 @@ +[ + { + "id": "003ec732-5bd5-4312-9c0a-533386288937", + "ruleTemplateId": "c75fa0c8-335e-4e16-b911-7ee8e3bb259d", + "ruleName": "networkPort139ExposedToInternetGCP_25082021", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retVal] {\n rule := input.google_compute_firewall[_]\n config := rule.config\n config.direction == \"INGRESS\"\n config.source_ranges[_] == \"0.0.0.0/0\"\n fire_rule := config.allow[_]\n fire_rule.protocol == \"{{.protocol}}\"\n fire_rule.ports[_] == \"{{.portNumber}}\"\n\n expected := [ item | item := validate_source(config.source_ranges[_]) ]\n\ttraverse := \"source_ranges\"\n\n retVal := {\n \"Id\": rule.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": traverse,\n \"Attribute\": traverse,\n \"AttributeDataType\": \"list\",\n \"Expected\": expected,\n \"Actual\": config.source_ranges\n }\n}\n\nvalidate_source(source) = value {\n\tsource == \"0.0.0.0/0\"\n value := \"\"\n}\nvalidate_source(source) = value {\n\tsource != \"0.0.0.0/0\"\n value := source\n}", + "ruleTemplateName": "networkPortExposedToInternetGCP_25082021", + "ruleArgument": "{\"name\":\"networkPort139ExposedToInternetGCP\",\"portNumber\":139,\"prefix\":\"\",\"protocol\":\"tcp\",\"suffix\":\"_25082021\"}", + "severity": "HIGH", + "vulnerability": "NetBios Session Service (TCP:139) is exposed to entire internet for Google Compute Firewall.", + "remediation": "Limit the access scope for NetBios Session Service to only allow access in internal networks and limited scope. If public interface exists, remove it and limit the access scope within the VNET only to applications or instances that requires access.", + "engineType": "terraform", + "provider": "gcp", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure NetBios Session Service (TCP:139) is not exposed to entire internet for Google Compute Firewall", + "category": "Infrastructure Security", + "policyRelevance": "NIST-800-53:SC-7(11)&SC-7(21) ISO-27001:A.9.1.2&A.13.1.1 SOC2:CC6.6", + "ruleReferenceId": "AC_GCP_0095", + "policy": "Accurics Security Best Practices for GCP v2", + "version": 2, + "custom": false, + "resourceType": "google_compute_firewall" + }, + { + "id": "04eac6e0-c02a-41d2-8fae-3a0ef74d55b0", + "ruleTemplateId": "473369ba-4e7d-43e1-b66c-73a5f92e8b87", + "ruleName": "awsNoRetentionPolicyCloudwatchLogGroup", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retVal] {\n cw_log_group := input.aws_cloudwatch_log_group[_]\n cw_log_group.config.retention_in_days == 0\n\n traverse := \"retention_in_days\"\n\n retVal := {\n \"Id\": cw_log_group.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": traverse,\n \"Attribute\": traverse,\n \"AttributeDataType\": \"int\",\n \"Expected\": 120,\n \"Actual\": cw_log_group.config.retention_in_days\n }\n}", + "ruleTemplateName": "awsNoRetentionPolicyCloudwatchLogGroup", + "ruleArgument": "{\"name\":\"awsNoRetentionPolicyCloudwatchLogGroup\",\"prefix\":\"\",\"suffix\":\"\"}", + "severity": "MEDIUM", + "vulnerability": "AWS CloudWatch Log Group does not have a retention policy set in order to establish how long log events are kept in AWS CloudWatch Logs.", + "remediation": "AWS CloudWatch Log Group can be set with a log retention policy using AWS Console.\n In AWS Console - \n 1. Sign in to AWS Console and go to the CloudWatch console.\n 2. Select Log Groups in the navigation pane.\n 3. Select the log group to update.\n 4. Select 'Edit retention' and change the log retention value to 120 days.", + "engineType": "terraform", + "provider": "aws", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure log retention policy is set for AWS CloudWatch Log Group", + "category": "Security Best Practices", + "policyRelevance": "", + "ruleReferenceId": "AC_AWS_0452", + "policy": "Accurics Security Best Practices for AWS v2", + "version": 2, + "custom": false, + "resourceType": "aws_cloudwatch_log_group" + }, + { + "id": "058ef84d-9ad0-4ea1-83eb-9fd2f3e8f2d8", + "ruleTemplateId": "86e88b41-a767-4b9f-9bbc-33a86bd9f6b7", + "ruleName": "ensureMcasInArm_25082021", + "ruleTemplate": "{{.prefix}}{{.name}}{{.suffix}}[retval] {\n security_center_settings := input.azurerm_security_center_setting[_]\n upper(security_center_settings.config.setting_name) != \"{{.value}}\"\n security_center_settings.config.enabled != true\n\n retval := {\n \"Id\": security_center_settings.id,\n \"ReplaceType\": \"edit\",\n \"CodeType\": \"attribute\",\n \"Traverse\": \"enabled\",\n \"Attribute\": \"enabled\",\n \"AttributeDataType\": \"bool\",\n \"Expected\": true,\n \"Actual\": security_center_settings.config.enabled\n }\n}", + "ruleTemplateName": "ensureProperSettings_25082021", + "ruleArgument": "{\"name\":\"ensureMcasInArm\",\"prefix\":\"\",\"suffix\":\"_25082021\",\"value\":\"MCAS\"}", + "severity": "MEDIUM", + "vulnerability": "Disabled MCAS in Azure Security Center Setting goes against compliance.", + "remediation": "Ensure MCAS setting is enabled in Azure Security Center Setting.", + "engineType": "terraform", + "provider": "azure", + "managedBy": "Accurics Inc", + "ruleDisplayName": "Ensure that Microsoft Cloud App Security (MCAS) integration is selected in Azure Security Center Setting", + "category": "Compliance Validation", + "policyRelevance": "CIS-1.3:2.10", + "ruleReferenceId": "AC_AZURE_0330", + "policy": "Accurics Security Best Practices for Azure v2", + "version": 2, + "custom": false, + "resourceType": "azurerm_security_center_setting" + } +] \ No newline at end of file diff --git a/pkg/initialize/types.go b/pkg/initialize/types.go new file mode 100644 index 000000000..1b85682b2 --- /dev/null +++ b/pkg/initialize/types.go @@ -0,0 +1,94 @@ +/* + Copyright (C) 2022 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 initialize + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/accurics/terrascan/pkg/policy" +) + +type environmentPolicy struct { + regoTemplate string + metadataFileName string + resourceType string + policyMetadata policy.RegoMetadata +} + +func newPolicy(ruleMetadata environmentPolicyMetadata) (environmentPolicy, error) { + var policy environmentPolicy + var templateArgs map[string]interface{} + + policy.regoTemplate = "package accurics\n\n" + ruleMetadata.RuleTemplate + policy.metadataFileName = ruleMetadata.RuleReferenceID + ".json" + policy.resourceType = ruleMetadata.ResourceType + + policy.policyMetadata.Name = ruleMetadata.RuleName + policy.policyMetadata.File = ruleMetadata.RegoName + ".rego" + policy.policyMetadata.ResourceType = ruleMetadata.ResourceType + policy.policyMetadata.Severity = ruleMetadata.Severity + policy.policyMetadata.Description = ruleMetadata.RuleDisplayName + policy.policyMetadata.ReferenceID = ruleMetadata.RuleReferenceID + policy.policyMetadata.ID = ruleMetadata.RuleReferenceID + policy.policyMetadata.Category = ruleMetadata.Category + policy.policyMetadata.Version = ruleMetadata.Version + + templateString, ok := ruleMetadata.RuleArgument.(string) + if !ok { + return policy, fmt.Errorf("incorrect rule argument type, must be a string") + } + err := json.Unmarshal([]byte(templateString), &templateArgs) + if err != nil { + return policy, fmt.Errorf("error occurred while unmarshaling rule arguments into map[string]interface{}, error: '%w'", err) + } + policy.policyMetadata.TemplateArgs = templateArgs + + return policy, nil +} + +func (p environmentPolicy) getType() string { + provider := strings.ToLower(p.resourceType) + + if strings.HasPrefix(provider, "azure") { + return "azure" + } + + if strings.HasPrefix(provider, "google") { + return "gcp" + } + + if strings.HasPrefix(provider, "kubernetes") { + return "k8s" + } + + return strings.Split(provider, "_")[0] +} + +type environmentPolicyMetadata struct { + RuleName string `json:"ruleName"` + RegoName string `json:"ruleTemplateName"` + RuleArgument interface{} `json:"ruleArgument"` + Severity string `json:"severity"` + RuleDisplayName string `json:"ruleDisplayName"` + Category string `json:"category"` + RuleReferenceID string `json:"ruleReferenceId"` + Version int `json:"version"` + RuleTemplate string `json:"ruleTemplate"` + ResourceType string `json:"resourceType"` +}