From 5d499fd3e177617f4ff6b223235017782bb6fad6 Mon Sep 17 00:00:00 2001 From: Richard Park Date: Tue, 7 Sep 2021 10:07:48 -0400 Subject: [PATCH 1/4] Initial commit --- .gitignore | 2 + pkg/loader/arm.go | 95 +++++++++++++++++++++++++++ pkg/loader/base.go | 7 +- pkg/loader/loadpaths.go | 3 + rego/lib/fugue/input_type.rego | 8 +++ rego/lib/fugue/resource_view.rego | 7 ++ rego/lib/fugue/resource_view/arm.rego | 23 +++++++ 7 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 pkg/loader/arm.go create mode 100644 rego/lib/fugue/resource_view/arm.rego diff --git a/.gitignore b/.gitignore index f336d807..7e9d1a91 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ dist/ .vscode/ .scratch/ .regula-history +testarm.json +testrules diff --git a/pkg/loader/arm.go b/pkg/loader/arm.go new file mode 100644 index 00000000..184a6202 --- /dev/null +++ b/pkg/loader/arm.go @@ -0,0 +1,95 @@ +// Copyright 2021 Fugue, 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 loader + +import ( + "encoding/json" + "fmt" +) + +var validArmExts map[string]bool = map[string]bool{ + ".json": true, +} + +type ArmDetector struct{} + +func (c *ArmDetector) DetectFile(i InputFile, opts DetectOptions) (IACConfiguration, error) { + if !opts.IgnoreExt && !validArmExts[i.Ext()] { + return nil, fmt.Errorf("File does not have .json extension: %v", i.Path()) + } + contents, err := i.Contents() + if err != nil { + return nil, err + } + + template := &armTemplate{} + if err := json.Unmarshal(contents, &template.Contents); err != nil { + return nil, fmt.Errorf("Failed to parse file as JSON %v: %v", i.Path(), err) + } + _, hasSchema := template.Contents["$schema"] + _, hasResources := template.Contents["resources"] + + if !hasSchema && !hasResources { + return nil, fmt.Errorf("Input file is not an ARM template: %v", i.Path()) + } + path := i.Path() + source := &CfnSourceInfo{} + // source, err := LoadCfnSourceInfo(path, contents) + // if err != nil { + // source = nil // Don't consider source code locations essential. + // } + + return &armConfiguration{ + path: path, + template: *template, + source: source, + }, nil +} + +func (c *ArmDetector) DetectDirectory(i InputDirectory, opts DetectOptions) (IACConfiguration, error) { + return nil, nil +} + +type armConfiguration struct { + path string + template armTemplate + source *CfnSourceInfo +} + +func (l *armConfiguration) RegulaInput() RegulaInput { + return RegulaInput{ + "filepath": l.path, + "content": l.template.Contents, + } +} + +func (l *armConfiguration) Location(attributePath []string) (LocationStack, error) { + if l.source == nil { + return nil, nil + } + loc, err := l.source.Location(attributePath) + if loc == nil || err != nil { + return nil, err + } + return []Location{*loc}, nil +} + +func (l *armConfiguration) LoadedFiles() []string { + return []string{l.path} +} + +type armTemplate struct { + Contents map[string]interface{} +} diff --git a/pkg/loader/base.go b/pkg/loader/base.go index 91041aa6..264b828a 100644 --- a/pkg/loader/base.go +++ b/pkg/loader/base.go @@ -41,6 +41,8 @@ const ( // Tf means that regula will load the HCL in the directory in a similar // way to terraform plan, or it can also load individual files. Tf + // Azure Resource Manager JSON + Arm ) // InputTypeIDs maps the InputType enums to string values that can be specified in @@ -50,6 +52,7 @@ var InputTypeIDs = map[InputType][]string{ TfPlan: {"tf-plan", "tf_plan"}, Cfn: {"cfn"}, Tf: {"tf"}, + Arm: {"arm"}, } // InputTypeForString is a utility function to translate the string name of an input @@ -64,6 +67,8 @@ func InputTypeForString(typeStr string) (InputType, error) { return TfPlan, nil case "tf": return Tf, nil + case "arm": + return Arm, nil default: return -1, fmt.Errorf("Unrecognized input type %v", typeStr) } @@ -114,7 +119,7 @@ type Location struct { // // These are stored as a call stack, with the most specific location in the // first position, and the "root of the call stack" at the last position. -type LocationStack = []Location; +type LocationStack = []Location func (l Location) String() string { return fmt.Sprintf("%s:%d:%d", l.Path, l.Line, l.Col) diff --git a/pkg/loader/loadpaths.go b/pkg/loader/loadpaths.go index 0109c94e..4d8adc9f 100644 --- a/pkg/loader/loadpaths.go +++ b/pkg/loader/loadpaths.go @@ -203,6 +203,7 @@ func DetectorByInputType(inputType InputType) (ConfigurationDetector, error) { &CfnDetector{}, &TfPlanDetector{}, &TfDetector{}, + &ArmDetector{}, ), nil case Cfn: return &CfnDetector{}, nil @@ -210,6 +211,8 @@ func DetectorByInputType(inputType InputType) (ConfigurationDetector, error) { return &TfPlanDetector{}, nil case Tf: return &TfDetector{}, nil + case Arm: + return &ArmDetector{}, nil default: return nil, fmt.Errorf("Unsupported input type: %v", inputType) } diff --git a/rego/lib/fugue/input_type.rego b/rego/lib/fugue/input_type.rego index ed7d6ea7..c03cd993 100644 --- a/rego/lib/fugue/input_type.rego +++ b/rego/lib/fugue/input_type.rego @@ -21,6 +21,7 @@ package fugue.input_type_internal # - "tf_plan" # - "tf_runtime" # - "cfn" +# - "arm" # # To check the current resource type, use `input_type`. # To check if a rule applies for this input type, use `compatibility`. @@ -37,6 +38,8 @@ input_type = "tf" { _ = input.Resources } else = "cfn" { _ = input.AWSTemplateFormatVersion +} else = "arm" { + _ = input.contentVersion } else = "unknown" { true } @@ -51,6 +54,10 @@ cloudformation_input_type { input_type == "cfn" } +arm_input_type { + input_type == "arm" +} + rule_input_type(pkg) = ret { # This is a workaround for an issue in fregot, where the next line will fail # the typechecker when there isn't a single `input_type` defined, which is @@ -69,4 +76,5 @@ compatibility := { "tf_runtime": {"tf_runtime"}, "cfn": {"cfn"}, "cloudformation": {"cfn"}, # Backwards-compatibility + "arm": {"arm"}, } diff --git a/rego/lib/fugue/resource_view.rego b/rego/lib/fugue/resource_view.rego index ea74dd1d..b4cd61e3 100644 --- a/rego/lib/fugue/resource_view.rego +++ b/rego/lib/fugue/resource_view.rego @@ -20,6 +20,7 @@ package fugue.resource_view import data.fugue.input_type_internal import data.fugue.resource_view.cloudformation import data.fugue.resource_view.terraform +import data.fugue.resource_view.arm resource_view = ret { # If we are already given a resource view, just pass it through. @@ -31,6 +32,9 @@ resource_view = ret { } else = ret { input_type_internal.cloudformation_input_type ret = cloudformation.resource_view +} else = ret { + input_type_internal.arm_input_type + ret = arm.resource_view } resource_view_input = ret { @@ -42,4 +46,7 @@ resource_view_input = ret { } else = ret { input_type_internal.cloudformation_input_type ret = {"resources": resource_view, "_template": input} +} else = ret { + input_type_internal.arm_input_type + ret = {"resources": resource_view, "_template": input} } diff --git a/rego/lib/fugue/resource_view/arm.rego b/rego/lib/fugue/resource_view/arm.rego new file mode 100644 index 00000000..7d2ad49c --- /dev/null +++ b/rego/lib/fugue/resource_view/arm.rego @@ -0,0 +1,23 @@ +# Copyright 2021 Fugue, 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 fugue.resource_view.arm + +resource_view[id] = ret { + resource := input.resources[id] + ret := json.patch(resource, [ + {"op": "add", "path": ["id"], "value": resource.name}, + {"op": "add", "path": ["_type"], "value": resource.type}, + {"op": "add", "path": ["_provider"], "value": "arm"}, + ]) +} \ No newline at end of file From 2111927de53577ba694752028f8bd7f447f3cef6 Mon Sep 17 00:00:00 2001 From: Richard Park Date: Tue, 7 Sep 2021 23:23:52 -0400 Subject: [PATCH 2/4] Modified .gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 7e9d1a91..f336d807 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,3 @@ dist/ .vscode/ .scratch/ .regula-history -testarm.json -testrules From 6f2bcf2d29ee396ceeda3bd0c54e5604800da8e3 Mon Sep 17 00:00:00 2001 From: Richard Park Date: Tue, 7 Sep 2021 23:35:59 -0400 Subject: [PATCH 3/4] Removed source from armConfiguration --- pkg/loader/arm.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/loader/arm.go b/pkg/loader/arm.go index 184a6202..31cffcda 100644 --- a/pkg/loader/arm.go +++ b/pkg/loader/arm.go @@ -45,16 +45,10 @@ func (c *ArmDetector) DetectFile(i InputFile, opts DetectOptions) (IACConfigurat return nil, fmt.Errorf("Input file is not an ARM template: %v", i.Path()) } path := i.Path() - source := &CfnSourceInfo{} - // source, err := LoadCfnSourceInfo(path, contents) - // if err != nil { - // source = nil // Don't consider source code locations essential. - // } return &armConfiguration{ path: path, template: *template, - source: source, }, nil } From 3457057c771549993f77fa41cb851811c13c9739 Mon Sep 17 00:00:00 2001 From: Richard Park Date: Mon, 4 Oct 2021 23:29:49 -0400 Subject: [PATCH 4/4] Added first few ARM rules --- rego/rules/arm/sql/auditing.rego | 47 ++++++ .../arm/sql/auditing_retention_90days.rego | 48 ++++++ testarm.json | 159 ++++++++++++++++++ testrules/testarmrule.rego | 10 ++ testrules/testarmrule2.rego | 10 ++ 5 files changed, 274 insertions(+) create mode 100644 rego/rules/arm/sql/auditing.rego create mode 100644 rego/rules/arm/sql/auditing_retention_90days.rego create mode 100644 testarm.json create mode 100644 testrules/testarmrule.rego create mode 100644 testrules/testarmrule2.rego diff --git a/rego/rules/arm/sql/auditing.rego b/rego/rules/arm/sql/auditing.rego new file mode 100644 index 00000000..95d9fb25 --- /dev/null +++ b/rego/rules/arm/sql/auditing.rego @@ -0,0 +1,47 @@ +# Copyright 2020-2021 Fugue, 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 rules.arm_sql_auditing + +import data.fugue + +__rego__metadoc__ := { + "custom": { + "controls": { + "CIS-Azure_v1.1.0": [ + "CIS-Azure_v1.1.0_4.1" + ], + "CIS-Azure_v1.3.0": [ + "CIS-Azure_v1.3.0_4.1.1" + ] + }, + "severity": "Medium" + }, + "description": "SQL Server auditing should be enabled. The Azure platform allows a SQL server to be created as a service. Enabling auditing at the server level ensures that all existing and newly created databases on the SQL server instance are audited. Auditing policy applied on the SQL database does not override auditing policy and settings applied on the particular SQL server where the database is hosted.", + "id": "FG_R00282", + "title": "SQL Server auditing should be enabled" +} + +input_type = "arm" + +resource_type = "Microsoft.Sql/servers/databases/auditingPolicies" + +default allow = false + +allow { + { + lower(input.properties.auditingState) == "enabled" + } { + lower(input.properties.state) == "enabled" + } +} \ No newline at end of file diff --git a/rego/rules/arm/sql/auditing_retention_90days.rego b/rego/rules/arm/sql/auditing_retention_90days.rego new file mode 100644 index 00000000..132a6474 --- /dev/null +++ b/rego/rules/arm/sql/auditing_retention_90days.rego @@ -0,0 +1,48 @@ +# Copyright 2020-2021 Fugue, 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 rules.arm_sql_auditing_retention_90days + +import data.fugue + +__rego__metadoc__ := { + "custom": { + "controls": { + "CIS-Azure_v1.1.0": [ + "CIS-Azure_v1.1.0_4.3" + ], + "CIS-Azure_v1.3.0": [ + "CIS-Azure_v1.3.0_4.1.3" + ] + }, + "severity": "Medium" + }, + "description": "SQL Server auditing retention should be greater than 90 days. Audit Logs can be used to check for anomalies and give insight into suspected breaches or misuse of information and access.", + "id": "FG_R00283", + "title": "SQL Server auditing retention should be greater than 90 days" +} + +input_type = "arm" + +resource_type = "Microsoft.Sql/servers/databases/auditingPolicies" + +default allow = false + +allow { + { + lower(input.properties.auditingState) == "enabled" + } { + lower(input.properties.state) == "enabled" + } + input.properties.retentionDays >= 90 +} \ No newline at end of file diff --git a/testarm.json b/testarm.json new file mode 100644 index 00000000..d29dd666 --- /dev/null +++ b/testarm.json @@ -0,0 +1,159 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2019-04-01", + "name": "myStorageAccount", + "location": "string", + "tags": { + "tagName1": "tagValue1", + "tagName2": "tagValue2" + }, + "sku": { + "name": "string" + }, + "kind": "string", + "extendedLocation": { + "name": "string", + "type": "EdgeZone" + }, + "identity": { + "type": "string", + "userAssignedIdentities": {} + }, + "properties": { + "accessTier": "Hot", + "allowBlobPublicAccess": "bool", + "allowCrossTenantReplication": "bool", + "allowSharedKeyAccess": "bool", + "azureFilesIdentityBasedAuthentication": { + "activeDirectoryProperties": { + "azureStorageSid": "string", + "domainGuid": "string", + "domainName": "string", + "domainSid": "string", + "forestName": "string", + "netBiosDomainName": "string" + }, + "defaultSharePermission": "string", + "directoryServiceOptions": "string" + }, + "customDomain": { + "name": "string", + "useSubDomainName": "bool" + }, + "encryption": { + "identity": { + "userAssignedIdentity": "string" + }, + "keySource": "string", + "keyvaultproperties": { + "keyname": "string", + "keyvaulturi": "string", + "keyversion": "string" + }, + "requireInfrastructureEncryption": "bool", + "services": { + "blob": { + "enabled": "bool", + "keyType": "string" + }, + "file": { + "enabled": "bool", + "keyType": "string" + }, + "queue": { + "enabled": "bool", + "keyType": "string" + }, + "table": { + "enabled": "bool", + "keyType": "string" + } + } + }, + "isHnsEnabled": "bool", + "isNfsV3Enabled": "bool", + "keyPolicy": { + "keyExpirationPeriodInDays": "int" + }, + "largeFileSharesState": "string", + "minimumTlsVersion": "string", + "networkAcls": { + "bypass": "string", + "defaultAction": "string", + "ipRules": [ + { + "action": "Allow", + "value": "string" + } + ], + "resourceAccessRules": [ + { + "resourceId": "string", + "tenantId": "string" + } + ], + "virtualNetworkRules": [ + { + "action": "Allow", + "id": "string", + "state": "string" + } + ] + }, + "routingPreference": { + "publishInternetEndpoints": "bool", + "publishMicrosoftEndpoints": "bool", + "routingChoice": "string" + }, + "sasPolicy": { + "expirationAction": "Log", + "sasExpirationPeriod": "string" + }, + "supportsHttpsTrafficOnly": "true" + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "name": "[concat('myvm', copyindex(int(parameters('offsetIndexValue'))))]", + "apiVersion": "2017-03-30", + "location": "[resourceGroup().location]", + "tags": { + "tag1": "vmtag1", + "tag2": "vmtag2", + "tag3": "vmtag3", + "tag4": "vmtag4", + "tag5": "vmtag5", + "tag6": "vmtag6", + "tag7": "vmtag7", + "tag8": "vmtag8", + "tag9": "vmtag9", + "tag10": "vmtag10", + "tag11": "vmtag11", + "tag12": "vmtag12", + "tag13": "vmtag13" + }, + "copy": { + "name": "virtualMachineLoop", + "count": "[parameters('numberOfInstances')]" + }, + "dependsOn": [ + "nicLoop", + "vmDiskResLoop" + ], + "properties": { + "hardwareProfile": { + "vmSize": "Standard_A0" + }, + "osProfile": { + "computerName": "[concat('vm', copyindex(int(parameters('offsetIndexValue'))))]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]" + } + } + } + ] +} \ No newline at end of file diff --git a/testrules/testarmrule.rego b/testrules/testarmrule.rego new file mode 100644 index 00000000..f051a7fa --- /dev/null +++ b/testrules/testarmrule.rego @@ -0,0 +1,10 @@ +package rules.storage_account_nohttps + +resource_type = "Microsoft.Storage/storageAccounts" +input_type = "arm" + +default allow = false + +allow { + input.properties.supportsHttpsTrafficOnly == "true" +} \ No newline at end of file diff --git a/testrules/testarmrule2.rego b/testrules/testarmrule2.rego new file mode 100644 index 00000000..97e29bdf --- /dev/null +++ b/testrules/testarmrule2.rego @@ -0,0 +1,10 @@ +package rules.virtual_machine_vmsize + +resource_type = "Microsoft.Compute/virtualMachines" +input_type = "arm" + +default allow = false + +allow { + input.properties.hardwareProfile.vmSize == "Standard_A0" +} \ No newline at end of file