diff --git a/pkg/loader/arm.go b/pkg/loader/arm.go new file mode 100644 index 00000000..31cffcda --- /dev/null +++ b/pkg/loader/arm.go @@ -0,0 +1,89 @@ +// 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() + + return &armConfiguration{ + path: path, + template: *template, + }, 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 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