Skip to content

Commit 7cdcb14

Browse files
committed
docs and support for sentinel apply
1 parent 7fe9afc commit 7cdcb14

23 files changed

+305
-107
lines changed

governance/second-generation/aws/sentinel.hcl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ policy "require-private-acl-and-kms-for-s3-buckets" {
66
enforcement_level = "advisory"
77
}
88

9+
policy "restrict-assumed-role-by-workspace" {
10+
enforcement_level = "advisory"
11+
}
12+
913
policy "restrict-assumed-role" {
1014
enforcement_level = "advisory"
1115
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Sentinel HTTP Import and Parameters Examples
2+
This directory contains examples of using the [HTTP import](https://docs.hashicorp.com/sentinel/imports/http) and [policy parameters](https://docs.hashicorp.com/sentinel/language/parameters) that were added in the Sentinel 0.13.0 runtime. Policy parameters allow you to specify API credentials without storing them in your policies which would be undesirable since policies are stored in VCS repositories.
3+
4+
## Policies
5+
There are currently two example policies in this directory:
6+
* [check-external-http-api.sentinel](./check-external-http-api.sentinel)
7+
* [use-latest-module-versions.sentinel](./use-latest-module-versions.sentinel)
8+
9+
The first policy simply uses the HTTP import to call an external API, https://yesno.wtf/api that randomly returns "yes" or "no" (but sometimes returns "maybe"). It also uses the recently added [case statement](https://docs.hashicorp.com/sentinel/language/spec/#case-statements) that provides a selection control mechanism to conditionally execute different logic based on the value of an argument.
10+
11+
You can test the first policy from this directory (after forking or cloning the repository and [installing the Sentinel CLI](https://docs.hashicorp.com/sentinel/intro/getting-started/install/)) with this command:
12+
```
13+
sentinel test -run=check -verbose
14+
```
15+
16+
The second policy uses the HTTP import to call the Terraform Registry [List Modules API](https://www.terraform.io/docs/registry/api.html#list-modules) against a Terraform Cloud or Terraform Enterprise server in order to determine the most recent version of each module in the [Private Module Registry](https://www.terraform.io/docs/cloud/registry/index.html) (PMR) of an organization on that server.
17+
18+
This policy also uses parameters as described below.
19+
20+
## Use of Parameters in use-latest-module-versions.sentinel
21+
The [use-latest-module-versions.sentinel](./use-latest-module-versions.sentinel) policy uses four parameters:
22+
* `public_registry` indicates whether the public Terraform registry is being used. This is `false` by default, but could be set to `true`.
23+
* `address` gives the address of the Terraform Cloud or Terraform Enterprise server. It defaults to `app.terraform.io` which is the address of the multi-tenant Terraform Cloud server that HashiCorp runs. You must specify a value for this if using a Terraform Enterprise server.
24+
* `organization` gives the name of an [organization](https://www.terraform.io/docs/cloud/users-teams-organizations/organizations.html) on the Terraform Cloud or Terraform Enterprise server specified by `address`. You must always specify a valid organization.
25+
* `token` gives a valid Terraform Cloud API token which can be a user, team, or organization token. See the [API tokens](https://www.terraform.io/docs/cloud/users-teams-organizations/api-tokens.html) document for more information.
26+
27+
### Using Parameters with the Sentinel CLI
28+
While parameters can currently be set with environment variables when using the `sentinel apply` command, they cannot be set with environment variables when using the `sentinel test` command.
29+
30+
Consequently, you **cannot** test this policy with the `sentinel test` command using the provided test cases and mocks since you will not have a token allowed to call the API against the specified Cloud-Operations organization.
31+
32+
You could test the policy with the `sentinel test` command if you edited the mocks to reference modules contained in a PMR in an organization on your own TFC or TFE organization or contained in the public registry and added your own valid API token to the test cases.
33+
34+
Since many readers won't have modules in their own TFC/TFE organization, we have provided a [sentinel.json](./sentinel.json) configuration file and an additional mock file [mocks/mock-tfconfig-fail-0.12.sentinel](./mocks/mock-tfconfig-fail-0.12.sentinel) that references modules from the [public Terraform registry](https://registry.terraform.io). These allow you to run the `sentinel apply` command to use the use-latest-module-versions.sentinel policy.
35+
36+
Specifically, you can run this command to test that the versions of the Azure modules from the public module registry are the latest:
37+
```
38+
sentinel apply use-latest-module-versions.sentinel -trace
39+
```
40+
You do not need a token when talking to the public registry, so the sentinel.json file sets `token` to an empty string.
41+
42+
The policy should fail since the mock does not use the most recent versions of the two modules. If you would like to see the policy pass, change the versions of the modules in mocks/mock-tfconfig-fail-0.12.sentinel to the most recent versions listed under https://registry.terraform.io/modules/Azure/network/azurerm and https://registry.terraform.io/modules/Azure/compute/azurerm. Currently, those are "2.0.0" and "1.3.0" respectively.
43+
44+
Note that the `sentinel test` and `sentinel apply` commands for testing/applying the use-latest-module-versions.sentinel policy **really** are making HTTP calls to the API endpoints to retrieve the list of matching modules in the registries. However, the mocks simulate which modules would actually be used by Terraform code.
45+
46+
You should **not** edit sentinel.json unless you also edit mocks/mock-tfconfig-fail-0.12.sentinel to reference actual modules in the registry and organization that sentinel.json refers to.
47+
48+
### Using Parameters with Terraform Cloud/Enterprise
49+
50+
If you wish to use the [use-latest-module-versions.sentinel](./use-latest-module-versions.sentinel) policy on a Terraform Cloud (TFC) or Terraform Enterprise (TFE) server, you need to specify values for the `organization` and `token` parameters when registering the policy set that contains this policy. Only do this if you have actually created some modules in the Private Module Registry (PMR) in an organization on your server and have Terraform code that uses them.
51+
52+
You can do this as follows:
53+
1. Copy the files [check-external-http-api.sentinel](./check-external-http-api.sentinel), [use-latest-module-versions.sentinel](./use-latest-module-versions.sentinel), and [sentinel.hcl](./sentinel.hcl) into a VCS repository. (Don't copy the file sentinel.json which is only for use with the Sentinel CLI.)
54+
1. Optionally edit the copy of sentinel.hcl to set the enforcement_level for the use-latest-module-versions policy to `soft-mandatory`.
55+
1. Commit the files to your VCS repository.
56+
1. Instead of doing the above 3 steps, you could fork the [test-http-policies-and-parameters](https://github.com/rberlind/test-http-policies-and-parameters) repository and use that fork.
57+
1. [Register a new policy set](https://www.terraform.io/docs/cloud/sentinel/manage-policies.html#managing-policy-sets) on your Terraform Cloud or Terraform Enterprise server.
58+
1. Edit the registered policy sets to specify values for the `organization` and `token` parameters making sure you pick an organization that actually has some modules in its PMR and that the token you give is a valid API token with permission in that organization. (You cannot specify parameters until after creating the policy set.) Parameters are added at the bottom of the Policy Set screen.
59+
1. Be sure to mark your `token` parameter as sensitive so that nobody else can see it in the Terraform Cloud UI.
60+
1. If using a Terraform Enterprise server, also specify a value for the `address` parameter, using a value like "tfe.example.com".
61+
1. Save the policy set.
62+
1. Add a workspace to the policy set that uses Terraform code that references modules in the PMR in the organization you specified.
63+
1. Queue a plan against that workspace in the Terraform Cloud UI.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import "strings"
2+
import "types"
3+
4+
_modules = {
5+
"root": {
6+
"data": {},
7+
"modules": {
8+
"first": {
9+
"config": {},
10+
"references": {},
11+
"source": "Azure/network/azurerm",
12+
"version": "1.1.1",
13+
},
14+
},
15+
"outputs": {},
16+
"providers": {},
17+
"resources": {},
18+
"variables": {},
19+
},
20+
21+
"module.first": {
22+
"data": {},
23+
"modules": {
24+
"second": {
25+
"config": {},
26+
"references": {},
27+
"source": "Azure/compute/azurerm",
28+
"version": "1.1.7",
29+
},
30+
},
31+
"outputs": {},
32+
"providers": {},
33+
"resources": {},
34+
"variables": {},
35+
},
36+
37+
"module.first.module.second": {
38+
"data": {},
39+
"modules": {},
40+
"outputs": {},
41+
"providers": {},
42+
"resources": {},
43+
"variables": {},
44+
},
45+
}
46+
47+
module_paths = [
48+
[],
49+
[
50+
"first",
51+
],
52+
[
53+
"first",
54+
"second",
55+
],
56+
]
57+
58+
module = func(path) {
59+
if types.type_of(path) is not "list" {
60+
error("expected list, got", types.type_of(path))
61+
}
62+
63+
if length(path) < 1 {
64+
return _modules.root
65+
}
66+
67+
addr = []
68+
for path as p {
69+
append(addr, "module")
70+
append(addr, p)
71+
}
72+
73+
return _modules[strings.join(addr, ".")]
74+
}
75+
76+
data = _modules.root.data
77+
modules = _modules.root.modules
78+
providers = _modules.root.providers
79+
resources = _modules.root.resources
80+
variables = _modules.root.variables
81+
outputs = _modules.root.outputs
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
policy "check-external-http-api" {
2+
enforcement_level = "advisory"
3+
}
4+
5+
policy "use-latest-module-versions" {
6+
enforcement_level = "advisory"
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"param": {
3+
"public_registry": true,
4+
"address": "registry.terraform.io",
5+
"organization": "Azure",
6+
"token": ""
7+
},
8+
"mock": {
9+
"tfconfig": "mocks/mock-tfconfig-fail-0.12.sentinel"
10+
}
11+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# This policy uses the HTTP import to call the TFC API to get a list of all
2+
# modules in a specified module registry and determine their
3+
# latest versions. It then uses the tfconfig import to inspect all non-root
4+
# modules and validate that those sourced from the registry use the latest versions.
5+
6+
# Note that this requires the Sentinel runtime 0.13.0 or higher.
7+
# Additionally, this policy uses Sentinel parameters
8+
# Be sure to read the associated use-latest-module-versions.md file.
9+
10+
##### Imports #####
11+
import "tfconfig"
12+
import "http"
13+
import "strings"
14+
import "json"
15+
import "types"
16+
17+
##### Parameters #####
18+
19+
# A boolean indicating whether the public Terraform registry is being used
20+
param public_registry default false
21+
22+
# The address of the Terraform Cloud or Terraform Enterprise server
23+
param address default "app.terraform.io"
24+
25+
# The name of the Terraform Cloud or Terraform Enterprise organization
26+
param organization
27+
28+
# A valid Terraform Cloud or Terraform Enterprise API token
29+
param token
30+
31+
32+
##### Functions #####
33+
34+
# Retrieve modules from a module registry and give their paths and versions
35+
retrieve_latest_module_versions = func(public_registry, address, organization, token) {
36+
37+
# Call the TFC Modules API and extract the response
38+
if public_registry {
39+
# We are limiting the request to 20 verified modules for the
40+
# namespace in the public registry matching the organzation parameter
41+
req = http.request("https://" + address + "/v1/modules/" +
42+
organization + "?limit=20&verified=true")
43+
} else {
44+
req = http.request("https://" + address + "/api/registry/v1/modules/" +
45+
organization)
46+
req = req.with_header("Authorization", "Bearer " + token)
47+
}
48+
res = json.unmarshal(http.get(req).body)
49+
50+
modules = {}
51+
52+
# Iterate over the modules and extract names, providers, and latest versions
53+
for res.modules as m {
54+
index = m.namespace + "/" + m.name + "/" + m.provider
55+
modules[index] = m.version
56+
}
57+
58+
# modules is indexed by <name>/<provider> and contains most recent version
59+
return modules
60+
}
61+
62+
# Validate sources of modules in the registry
63+
validate_modules = func(public_registry, address, organization, token) {
64+
65+
validated = true
66+
67+
# Get latest module versions from registry
68+
discovered_modules = retrieve_latest_module_versions(public_registry, address,
69+
organization, token)
70+
71+
# Iterate over all modules in the tfconfig import
72+
for tfconfig.module_paths as path {
73+
the_modules = tfconfig.module(path).modules
74+
# Iterate over modules of the current module
75+
for the_modules as name, m {
76+
# Check if module is in the PMR
77+
if strings.has_prefix(m.source, address + "/" + organization) {
78+
# Check version of module against latest version in PMR
79+
org_name_provider = strings.trim_prefix(m.source, address + "/")
80+
if m.version is not discovered_modules[org_name_provider] {
81+
if length(path) == 0 {
82+
# root module
83+
print("PMR module", m.source, "used in root module has version",
84+
m.version, "that is not the most recent version",
85+
discovered_modules[org_name_provider])
86+
validated = false
87+
} else {
88+
# non-root module
89+
module_address = "module." + strings.join(path, ".module.")
90+
print("PMR module", m.source, "used in module", module_address,
91+
"has version", m.version, "that is not the most recent",
92+
"version", discovered_modules[org_name_provider])
93+
validated = false
94+
}
95+
96+
} // end version check
97+
} else {
98+
# Check version of module against latest version in public registry
99+
if m.source in keys(discovered_modules) and
100+
m.version is not discovered_modules[m.source] {
101+
if length(path) == 0 {
102+
# root module
103+
print("Public registry module", m.source, "used in root module",
104+
"has version", m.version, "that is not the most recent version",
105+
discovered_modules[m.source])
106+
validated = false
107+
} else {
108+
# non-root module
109+
module_address = "module." + strings.join(path, ".module.")
110+
print("Public registry module", m.source, "used in module", module_address,
111+
"has version", m.version, "that is not the most recent",
112+
"version", discovered_modules[m.source])
113+
validated = false
114+
}
115+
116+
} // end version check
117+
118+
} // end if module in PMR or public registry
119+
} // end for modules
120+
} // end module paths
121+
122+
return validated
123+
}
124+
125+
##### Rules #####
126+
127+
# Main rule
128+
modules_validated = validate_modules(public_registry, address, organization, token)
129+
main = rule {
130+
modules_validated
131+
}

governance/second-generation/cloud-agnostic/sentinel.hcl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
policy "blacklist-datasources" {
2+
enforcement_level = "advisory"
3+
}
4+
5+
policy "blacklist-providers" {
6+
enforcement_level = "advisory"
7+
}
8+
19
policy "blacklist-provisioners" {
210
enforcement_level = "advisory"
311
}

governance/second-generation/cloud-agnostic/use-latest-module-versions.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)