-
Notifications
You must be signed in to change notification settings - Fork 240
/
restrict-resources-by-module-source.sentinel
145 lines (129 loc) · 5.52 KB
/
restrict-resources-by-module-source.sentinel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# This policy restricts resources of specific types to only be created in
# modules with sources in a given list of modules from public and private module
# registries or in their nested modules.
# If you want to allow creation of the resources in the root module, include
# "root" in the `allowed_module_sources` list. But you generally would not
# want to allow "root" since that sacrifices most control over creation of
# the resource types in `restricted_resources`.
# Note that this policy allows creation of resources in module blocks that
# point directly against a sub-directory of a module with a reference like
# "app.terraform.io/Cloud-Operations/s3-bucket/aws//modules/notification".
##### Imports #####
# Import common-functions/tfconfig-functions/tfconfig-functions.sentinel
# with alias "config"
import "tfconfig-functions" as config
# Strings import
import "strings"
##### Parameters #####
# Resources that should be restricted
param restricted_resources default [
"aws_s3_bucket",
"aws_s3_bucket_object",
"aws_s3_bucket_notification",
"azurerm_storage_account",
"azurerm_storage_container",
"azurerm_storage_blob",
"google_storage_bucket",
"google_storage_bucket_object",
]
# Allowed Public and Private Module Registry Modules
# These can have 3 or 4 segments delimited by "/".
# Assuming 4 segments for a Private Module Registry module:
# The first segment should be `app.terraform.io` for Terraform Cloud or the DNS
# of your TFE server, or `localterraform.com` to match the local TFE server.
# The second segement should be a TFC/E organization or `*` to allow any org.
# But `*` should only be used for TFE servers, not on Terraform Cloud.
# The third segment should be the name of the module.
# The fourth segment should be the provider of the module.
# Assuming 3 segments for a public registry module:
# The first segement should be the namespace of the module.
# The second segment should be the name of the module.
# The third segment should be the provider of the module.
# Do not add module sub-directories prefaced with "//" after the above segments.
param allowed_module_sources default [
"terraform-aws-modules/s3-bucket/aws",
"app.terraform.io/Cloud-Operations/s3-bucket/aws",
"localterraform.com/*/s3-bucket/aws",
"tfe.acme.com/*/s3-bucket/aws",
"app.terraform.io/Cloud-Operations/caf/azurerm",
"localterraform.com/*/caf/azurerm",
"app.terraform.io/Cloud-Operations/cloud-storage/google",
"localterraform.com/*/cloud-storage/google",
]
# Initialize validated. We'll flip to false if we find a violation.
validated = true
# Iterate over restricted resource types
for restricted_resources as _, type {
# Find all resources of the given type
all_resources = config.find_resources_by_type(type)
# Iterate over the resources to find module source
for all_resources as address, r {
# Get module source
module_address = r.module_address
module_source = config.get_ancestor_module_source(module_address)
#module_source = config.get_module_source(module_address)
# Initialize found_match to false. We'll flip to true if we find a match.
found_match = false
# Iterate over allowed sources
for allowed_module_sources as ams {
# Parse the allowed module source
ams_segments= strings.split(ams, "/")
num_ams_segments = length(ams_segments)
if num_ams_segments is 4 {
ams_host = ams_segments[0]
ams_org = ams_segments[1]
ams_name = ams_segments[2]
ams_provider = ams_segments[3]
} else if num_ams_segments is 3 {
ams_host = ""
ams_org = ams_segments[0]
ams_name = ams_segments[1]
ams_provider = ams_segments[2]
} else {
print("Module sources listed in the list allowed_module_sources should",
"only have 3 or 4 segments delimited by `/`, representing modules",
"from the public Terraform Registry in the first case or a Private",
"Module Registry in a Terraform Cloud or Terraform Enterprise",
"organization in the second case.")
print("Ignoring module source", ams)
continue
}
# Derive regex from the allowed module source
h_segments = strings.split(ams_host, ".")
if num_ams_segments is 4 {
h_reg = "^" + strings.join(h_segments, "\\.") + "/"
} else {
h_reg = "^" + strings.join(h_segments, "\\.")
}
if ams_org is "*" {
o_reg = "[A-Za-z0-9_-]+" + "/"
} else {
o_reg = ams_org + "/"
}
n_reg = ams_name + "/"
p_reg = ams_provider
# This regex allows module sources like "app.terraform.io/*/s3-bucket/aws"
# as well as "app.terraform.io/*/s3-bucket/aws//extra/path" so that nested
# modules of allowed modules can also be used.
ams_reg = "(" + h_reg + o_reg + n_reg + p_reg + "$|" +
h_reg + o_reg + n_reg + p_reg + "//(.*)$)"
#print("ams_reg:", ams_reg)
# Test module_source against the regex
if module_source matches ams_reg {
#print("module_source:", module_source)
#print("matched regex:", ams_reg)
found_match = true
break
} // end if module source match
} // end for allowed_module_sources
if not found_match {
print("resource", address, "has module source", module_source, "that is",
"not in the allowed list of modules:", allowed_module_sources)
validated = false
} // end if module_source in allowed_module_sources
} // end for all_resources
} // end restricted_resources
# Main rule
main = rule {
validated
}