Skip to content

Commit 746199d

Browse files
committed
update restrict-resources-by-module-source policy
1 parent 958a5ac commit 746199d

20 files changed

+7653
-436
lines changed

governance/third-generation/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ The `tfconfig-functions` module has several types of functions:
9292
* `items`: a map consisting of items that violate a condition.
9393
* `messages`: a map of violation messages associated with the items.
9494
* The same `to_string` and `print_violations` functions that are in the tfplan-functions module.
95-
* A `get_module_source` function that computes the source of a module from its address. This is used in the [restrict-resources-by-module-source.sentinel](./cloud-agnostic/restrict-resources-by-module-source.sentinel) policy to restrict creation of resources based on the actual module sources.
95+
* A `get_module_source` function that computes the source of a module from its address.
96+
* A `get_ancestor_module_source` function that computes the source of the first ancestor module that is not a local module of a module from its address. This is used in the [restrict-resources-by-module-source.sentinel](./cloud-agnostic/restrict-resources-by-module-source.sentinel) policy to restrict creation of resources based on the actual module sources.
97+
* A `get_parent_module_address` function that computes the address of the parent module of a module from its address.
9698

9799
Documentation for each individual function can be found in this directory:
98100
* [tfconfig-functions](./common-functions/tfconfig-functions/docs)

governance/third-generation/cloud-agnostic/restrict-resources-by-module-source.sentinel

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,63 @@
11
# This policy restricts resources of specific types to only be created in
2-
# modules with sources in a given list.
2+
# modules with sources in a given list of modules from public and private module
3+
# registries or in their nested modules.
34
# If you want to allow creation of the resources in the root module, include
45
# "root" in the `allowed_module_sources` list. But you generally would not
56
# want to allow "root" since that sacrifices most control over creation of
67
# the resource types in `restricted_resources`.
8+
# Note that this policy allows creation of resources in module blocks that
9+
# point directly against a sub-directory of a module with a reference like
10+
# "app.terraform.io/Cloud-Operations/s3-bucket/aws//modules/notification".
711

812
##### Imports #####
913

1014
# Import common-functions/tfconfig-functions/tfconfig-functions.sentinel
1115
# with alias "config"
1216
import "tfconfig-functions" as config
1317

18+
# Strings import
19+
import "strings"
20+
1421
##### Parameters #####
22+
23+
# Resources that should be restricted
1524
param restricted_resources default [
1625
"aws_s3_bucket",
1726
"aws_s3_bucket_object",
27+
"aws_s3_bucket_notification",
1828
"azurerm_storage_account",
1929
"azurerm_storage_container",
2030
"azurerm_storage_blob",
2131
"google_storage_bucket",
2232
"google_storage_bucket_object",
2333
]
2434

35+
# Allowed Public and Private Module Registry Modules
36+
# These can have 3 or 4 segments delimited by "/".
37+
# Assuming 4 segments for a Private Module Registry module:
38+
# The first segment should be `app.terraform.io` for Terraform Cloud or the DNS
39+
# of your TFE server, or `localterraform.com` to match the local TFE server.
40+
# The second segement should be a TFC/E organization or `*` to allow any org.
41+
# But `*` should only be used for TFE servers, not on Terraform Cloud.
42+
# The third segment should be the name of the module.
43+
# The fourth segment should be the provider of the module.
44+
# Assuming 3 segments for a public registry module:
45+
# The first segement should be the namespace of the module.
46+
# The second segment should be the name of the module.
47+
# The third segment should be the provider of the module.
48+
# Do not add module sub-directories prefaced with "//" after the above segments.
2549
param allowed_module_sources default [
50+
"terraform-aws-modules/s3-bucket/aws",
2651
"app.terraform.io/Cloud-Operations/s3-bucket/aws",
27-
"localterraform.com/Cloud-Operations/s3-bucket/aws",
52+
"localterraform.com/*/s3-bucket/aws",
53+
"tfe.acme.com/*/s3-bucket/aws",
2854
"app.terraform.io/Cloud-Operations/caf/azurerm",
29-
"localterraform.com/Cloud-Operations/caf/azurerm",
55+
"localterraform.com/*/caf/azurerm",
3056
"app.terraform.io/Cloud-Operations/cloud-storage/google",
31-
"localterraform.com/Cloud-Operations/cloud-storage/google",
57+
"localterraform.com/*/cloud-storage/google",
3258
]
3359

34-
# Initialize validated
60+
# Initialize validated. We'll flip to false if we find a violation.
3561
validated = true
3662

3763
# Iterate over restricted resource types
@@ -41,13 +67,73 @@ for restricted_resources as _, type {
4167

4268
# Iterate over the resources to find module source
4369
for all_resources as address, r {
44-
module_address = r.module_address
70+
4571
# Get module source
46-
module_source = config.get_module_source(module_address)
47-
# Check module_source
48-
if module_source not in allowed_module_sources {
49-
print("resource", address, "has module source", module_source,
50-
"that is not in the allowed list:", allowed_module_sources)
72+
module_address = r.module_address
73+
module_source = config.get_ancestor_module_source(module_address)
74+
#module_source = config.get_module_source(module_address)
75+
76+
# Initialize found_match to false. We'll flip to true if we find a match.
77+
found_match = false
78+
79+
# Iterate over allowed sources
80+
for allowed_module_sources as ams {
81+
82+
# Parse the allowed module source
83+
ams_segments= strings.split(ams, "/")
84+
num_ams_segments = length(ams_segments)
85+
if num_ams_segments is 4 {
86+
ams_host = ams_segments[0]
87+
ams_org = ams_segments[1]
88+
ams_name = ams_segments[2]
89+
ams_provider = ams_segments[3]
90+
} else if num_ams_segments is 3 {
91+
ams_host = ""
92+
ams_org = ams_segments[0]
93+
ams_name = ams_segments[1]
94+
ams_provider = ams_segments[2]
95+
} else {
96+
print("Module sources listed in the list allowed_module_sources should",
97+
"only have 3 or 4 segments delimited by `/`, representing modules",
98+
"from the public Terraform Registry in the first case or a Private",
99+
"Module Registry in a Terraform Cloud or Terraform Enterprise",
100+
"organization in the second case.")
101+
print("Ignoring module source", ams)
102+
continue
103+
}
104+
105+
# Derive regex from the allowed module source
106+
h_segments = strings.split(ams_host, ".")
107+
if num_ams_segments is 4 {
108+
h_reg = "^" + strings.join(h_segments, "\\.") + "/"
109+
} else {
110+
h_reg = "^" + strings.join(h_segments, "\\.")
111+
}
112+
if ams_org is "*" {
113+
o_reg = "[A-Za-z0-9_-]+" + "/"
114+
} else {
115+
o_reg = ams_org + "/"
116+
}
117+
n_reg = ams_name + "/"
118+
p_reg = ams_provider
119+
# This regex allows module sources like "app.terraform.io/*/s3-bucket/aws"
120+
# as well as "app.terraform.io/*/s3-bucket/aws//extra/path" so that nested
121+
# modules of allowed modules can also be used.
122+
ams_reg = "(" + h_reg + o_reg + n_reg + p_reg + "$|" +
123+
h_reg + o_reg + n_reg + p_reg + "//(.*)$)"
124+
#print("ams_reg:", ams_reg)
125+
126+
# Test module_source against the regex
127+
if module_source matches ams_reg {
128+
#print("module_source:", module_source)
129+
#print("matched regex:", ams_reg)
130+
found_match = true
131+
break
132+
} // end if module source match
133+
} // end for allowed_module_sources
134+
if not found_match {
135+
print("resource", address, "has module source", module_source, "that is",
136+
"not in the allowed list of modules:", allowed_module_sources)
51137
validated = false
52138
} // end if module_source in allowed_module_sources
53139
} // end for all_resources
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module "tfconfig-functions" {
2+
source = "../../../common-functions/tfconfig-functions/tfconfig-functions.sentinel"
3+
}
4+
5+
mock "tfconfig/v2" {
6+
module {
7+
source = "mock-tfconfig-fail-direct-nested-module.sentinel"
8+
}
9+
}
10+
11+
test {
12+
rules = {
13+
main = false
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module "tfconfig-functions" {
2+
source = "../../../common-functions/tfconfig-functions/tfconfig-functions.sentinel"
3+
}
4+
5+
mock "tfconfig/v2" {
6+
module {
7+
source = "mock-tfconfig-fail-nested-modules-2.sentinel"
8+
}
9+
}
10+
11+
test {
12+
rules = {
13+
main = false
14+
}
15+
}

governance/third-generation/cloud-agnostic/test/restrict-resources-by-module-source/fail.hcl renamed to governance/third-generation/cloud-agnostic/test/restrict-resources-by-module-source/fail-nested-modules.hcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module "tfconfig-functions" {
44

55
mock "tfconfig/v2" {
66
module {
7-
source = "mock-tfconfig-fail.sentinel"
7+
source = "mock-tfconfig-fail-nested-modules.sentinel"
88
}
99
}
1010

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module "tfconfig-functions" {
2+
source = "../../../common-functions/tfconfig-functions/tfconfig-functions.sentinel"
3+
}
4+
5+
mock "tfconfig/v2" {
6+
module {
7+
source = "mock-tfconfig-fail-non-nested-modules.sentinel"
8+
}
9+
}
10+
11+
test {
12+
rules = {
13+
main = false
14+
}
15+
}

0 commit comments

Comments
 (0)