A Terraform provider for Dokploy
Plugin-framework provider covering projects, environments, applications, Compose stacks, managed databases, domains, mounts, ports, redirects, basic auth, registries, SSH keys, certificates and backup destinations, over Dokploy's tRPC-over-REST API. The shim package exposes the provider to other Go modules, which is how pulumi-dokploy bridges it.
This commit is contained in:
3
.agents/skills/terraform-policy/.gitignore
vendored
Normal file
3
.agents/skills/terraform-policy/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
*.pyc
|
||||
41
.agents/skills/terraform-policy/README.md
Normal file
41
.agents/skills/terraform-policy/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Terraform Policy Agent Skills
|
||||
|
||||
A family of focused agent skills for working with [Terraform Policy](https://developer.hashicorp.com/terraform/cloud-docs/policy-enforcement) — HCP Terraform's native policy-as-code engine for `.policy.hcl` and `.policytest.hcl` files.
|
||||
|
||||
## Routing
|
||||
|
||||
Pick the skill that matches the user's journey:
|
||||
|
||||
| Journey | Reference |
|
||||
| --- | --- |
|
||||
| Write a new Terraform Policy from an English description | [**tfpolicy-author**](references/tfpolicy-author.md) |
|
||||
| Translate Sentinel (or adjacent OPA/Rego) to Terraform Policy | [**tfpolicy-author**](references/tfpolicy-author.md) |
|
||||
| Write or debug a `.policytest.hcl` test, mock resources, reason about the runner | [**tfpolicy-test**](references/tfpolicy-test.md) |
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
terraform-policy/
|
||||
├── SKILL.md # Router — routes to references below
|
||||
├── references/
|
||||
│ ├── tfpolicy-author.md # Authoring + Sentinel conversion (v0.2.0)
|
||||
│ ├── tfpolicy-test.md # Testing + full testing guide
|
||||
│ └── verified-syntax.md # Shared source-of-truth syntax reference
|
||||
├── examples/
|
||||
│ └── conversion/ # Side-by-side .sentinel / .policy.hcl examples
|
||||
└── evals/
|
||||
├── eval.yaml
|
||||
└── tasks/
|
||||
```
|
||||
|
||||
## Shared reference
|
||||
|
||||
[`references/verified-syntax.md`](references/verified-syntax.md) is the single source of truth for verified Terraform Policy syntax, function names, and runtime limitations. All reference files link to it rather than duplicating facts — when reference content disagrees with this file, the reference wins.
|
||||
|
||||
## Versioning
|
||||
|
||||
Each reference is versioned independently via its `metadata.version` field.
|
||||
|
||||
## License
|
||||
|
||||
MPL-2.0. Copyright IBM Corp. 2026.
|
||||
46
.agents/skills/terraform-policy/SKILL.md
Normal file
46
.agents/skills/terraform-policy/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: terraform-policy
|
||||
description: "Write, test, or convert Terraform Policy files (.policy.hcl, .policytest.hcl, Sentinel→tfpolicy). Triggers: policy.hcl, policytest, convert sentinel, tfpolicy, write a policy."
|
||||
license: MPL-2.0
|
||||
metadata:
|
||||
copyright: Copyright IBM Corp. 2026
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# terraform-policy
|
||||
|
||||
**UTILITY SKILL** — INVOKES: [tfpolicy-author](references/tfpolicy-author.md) | [tfpolicy-test](references/tfpolicy-test.md)
|
||||
|
||||
## USE FOR:
|
||||
|
||||
- Writing a new `.policy.hcl` policy from a description or requirement
|
||||
- Converting a `.sentinel` policy to Terraform Policy
|
||||
- Writing or debugging a `.policytest.hcl` test file
|
||||
- Migrating a Sentinel policy library to Terraform Policy
|
||||
|
||||
## DO NOT USE FOR:
|
||||
|
||||
- Writing `.tftest.hcl` files for Terraform modules — use `terraform-test`
|
||||
- General Terraform HCL authoring — use `terraform-style-guide`
|
||||
|
||||
## Routing
|
||||
|
||||
| Task | Sub-skill |
|
||||
|------|-----------|
|
||||
| Write or convert a `.policy.hcl` policy | [tfpolicy-author](references/tfpolicy-author.md) |
|
||||
| Write or debug a `.policytest.hcl` test | [tfpolicy-test](references/tfpolicy-test.md) |
|
||||
|
||||
## Examples
|
||||
|
||||
- "Block EC2 instances without encryption" → [tfpolicy-author](references/tfpolicy-author.md)
|
||||
- "Convert this Sentinel policy to tfpolicy" → [tfpolicy-author](references/tfpolicy-author.md)
|
||||
- "Write a policytest for my EBS policy" → [tfpolicy-test](references/tfpolicy-test.md)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Wrong skill triggered?** Load the sub-skill directly from the routing table above.
|
||||
|
||||
```bash
|
||||
npx skills add hashicorp/agent-skills/terraform/terraform-policy/skills/tfpolicy-author
|
||||
npx skills add hashicorp/agent-skills/terraform/terraform-policy/skills/tfpolicy-test
|
||||
```
|
||||
27
.agents/skills/terraform-policy/evals/eval.yaml
Normal file
27
.agents/skills/terraform-policy/evals/eval.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: terraform-policy-eval
|
||||
description: Auto-generated eval for terraform-policy.
|
||||
skill: terraform-policy
|
||||
version: "1.0"
|
||||
|
||||
config:
|
||||
trials_per_task: 1
|
||||
timeout_seconds: 300
|
||||
parallel: false
|
||||
executor: copilot-sdk
|
||||
model: claude-sonnet-4.6
|
||||
metrics:
|
||||
- name: task_completion
|
||||
weight: 0.7
|
||||
threshold: 0.8
|
||||
description: Did the skill complete trigger and anti-trigger checks?
|
||||
- name: efficiency
|
||||
weight: 0.3
|
||||
threshold: 0.7
|
||||
description: Did the skill stay within behavior limits?
|
||||
graders:
|
||||
- type: behavior
|
||||
name: token-budget
|
||||
config:
|
||||
max_tokens: 35000
|
||||
tasks:
|
||||
- "tasks/*.yaml"
|
||||
@@ -0,0 +1,17 @@
|
||||
id: negative-trigger-001
|
||||
name: Negative Trigger 1
|
||||
description: Auto-generated negative-trigger task.
|
||||
tags:
|
||||
- negative-trigger
|
||||
inputs:
|
||||
prompt: "Tell me a short joke about coffee."
|
||||
expected:
|
||||
should_trigger: false
|
||||
graders:
|
||||
- type: text
|
||||
name: omits-skill-keywords
|
||||
config:
|
||||
not_contains:
|
||||
- "policy"
|
||||
- "terraform"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
id: positive-trigger-001
|
||||
name: Positive Trigger 1
|
||||
description: Auto-generated positive-trigger task.
|
||||
tags:
|
||||
- positive-trigger
|
||||
inputs:
|
||||
prompt: "Use terraform-policy to help me complete this task"
|
||||
expected:
|
||||
should_trigger: true
|
||||
graders:
|
||||
- type: text
|
||||
name: contains-keywords
|
||||
config:
|
||||
contains:
|
||||
- "policy"
|
||||
- "terraform"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
id: positive-trigger-002
|
||||
name: Positive Trigger 2
|
||||
description: Auto-generated positive-trigger task.
|
||||
tags:
|
||||
- positive-trigger
|
||||
inputs:
|
||||
prompt: "I need assistance with terraform policy-related work"
|
||||
expected:
|
||||
should_trigger: true
|
||||
graders:
|
||||
- type: text
|
||||
name: contains-keywords
|
||||
config:
|
||||
contains:
|
||||
- "policy"
|
||||
- "terraform"
|
||||
|
||||
30
.agents/skills/terraform-policy/examples/README.md
Normal file
30
.agents/skills/terraform-policy/examples/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Sentinel to tfpolicy Conversion Examples
|
||||
|
||||
This folder packages representative Sentinel-to-tfpolicy conversion examples for sharing with teammates.
|
||||
|
||||
Each example subfolder contains:
|
||||
- `<sentinel-policy-name>.sentinel` - the actual Sentinel policy file included for comparison
|
||||
- `<sentinel-policy-name>.policy.hcl` - the tfpolicy version or best approximation
|
||||
- `README.md` - explanation of the conversion quality, what changed, and any limitations
|
||||
|
||||
Converted tfpolicy examples in this bundle prefer remediation-focused diagnostics over repeating Terraform addresses from Sentinel `summary {}` output. Terraform Policy diagnostics already identify the failing object and point to the relevant location, so converted examples avoid `${meta.address}` in error messages.
|
||||
|
||||
Included examples:
|
||||
- `dms-endpoints-should-use-ssl` - direct attribute conversion (`Perfect`)
|
||||
- `elasticsearch-https-required` - nested block conversion (`Good`)
|
||||
- `eventbridge-custom-event-bus-should-have-attached-policy` - cross-resource conversion via `core::getresources()` (`Limited`)
|
||||
- `cloudfront-associated-with-waf` - approximation only due to missing reference metadata (`Not convertible` as an exact translation)
|
||||
- `efs-access-point-should-enforce-user-identity` - direct presence check (`Perfect`)
|
||||
- `elasticsearch-encrypted-at-rest` - nested encryption block check (`Good`)
|
||||
- `dms-endpoint-should-be-ssl-configured` - config-derived certificate check (`Good`)
|
||||
- `ec2-network-acl-should-have-subnet-ids` - association-aware approximation (`Limited`)
|
||||
- `secretsmanager-auto-rotation-enabled-check` - secret-to-rotation relationship via `core::getresources()` (`Good`)
|
||||
- `s3-bucket-should-have-object-lock-enabled` - object lock association approximation (`Limited`)
|
||||
- `ec2-vpc-default-security-group-no-traffic` - inline-only approximation of a broader graph check (`Not convertible` as an exact translation)
|
||||
- `elasticsearch-in-vpc-only` - config-to-end-state VPC placement approximation (`Limited`)
|
||||
- `cloudtrail-server-side-encryption-enabled` - config-to-end-state encryption check (`Good`)
|
||||
- `step-functions-state-machine-logging-enabled` - nested logging block conversion (`Good`)
|
||||
- `elasticache-redis-replication-group-encryption-at-transit-enabled` - direct boolean check (`Perfect`)
|
||||
- `s3-block-public-access-bucket-level` - variable and association heavy approximation (`Not convertible` as an exact translation)
|
||||
|
||||
Note: The Sentinel policy files in this bundle come from the locally cloned policy library so reviewers can inspect the original Sentinel and converted tfpolicy side by side in one place.
|
||||
@@ -0,0 +1,19 @@
|
||||
# CloudFront Associated with WAF
|
||||
|
||||
## Source Sentinel Policy
|
||||
`cloudfront-associated-with-waf.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Not convertible` as an exact translation
|
||||
|
||||
## What the approximation does
|
||||
The included tfpolicy approximation checks only that `web_acl_id` is set to a non-empty value on `aws_cloudfront_distribution` resources.
|
||||
|
||||
## Why exact conversion is not possible today
|
||||
The Sentinel policy uses `tfconfig/v2` plus reference metadata (`references`) to reason about whether the CloudFront distribution is associated with a WAF resource. Current tfpolicy guidance does not expose equivalent reference metadata, so it cannot distinguish:
|
||||
- literal values
|
||||
- references to WAF resources
|
||||
- computed values
|
||||
|
||||
## Key limitation
|
||||
This means tfpolicy can enforce presence of a `web_acl_id`, but it cannot safely reproduce the Sentinel policy's reference-aware behavior.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Approximation of HashiCorp PCI DSS Sentinel example: cloudfront-associated-with-waf.sentinel
|
||||
# Exact conversion quality: Not convertible
|
||||
# This tfpolicy only checks for a non-empty web_acl_id value.
|
||||
|
||||
resource_policy "aws_cloudfront_distribution" "require_web_acl_id" {
|
||||
locals {
|
||||
web_acl_id = core::try(attrs.web_acl_id, "")
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.web_acl_id != ""
|
||||
error_message = "CloudFront distributions should set web_acl_id to associate a WAF or WAF Classic ACL"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// This policy checks whether 'aws_cloudfront_distribution' are associated with either AWS WAF Classic or AWS WAF web ACLs.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
// Imports
|
||||
|
||||
import "tfconfig/v2" as tfconfig
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
// Constants
|
||||
|
||||
const = {
|
||||
"policy_name": "cloudfront-associated-with-waf",
|
||||
"message": "'aws_cloudfront_distribution' are associated with either AWS WAF Classic or AWS WAF web ACLs. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/cloudfront-controls.html#cloudfront-6 for more details.",
|
||||
"resource_aws_cloudfront_distribution": "aws_cloudfront_distribution",
|
||||
}
|
||||
|
||||
// Functions
|
||||
|
||||
get_violations = func(resources) {
|
||||
return collection.reject(resources, func(res) {
|
||||
web_acl_id = maps.get(res.config, "web_acl_id", {})
|
||||
if web_acl_id is null or web_acl_id is empty {
|
||||
return false
|
||||
}
|
||||
references = maps.get(web_acl_id, "references", [])
|
||||
return references is not empty
|
||||
})
|
||||
}
|
||||
|
||||
// Variables
|
||||
|
||||
config_resources = tf.config(tfconfig.resources)
|
||||
cloudfront_distribution_resource = config_resources.type(const.resource_aws_cloudfront_distribution).resources
|
||||
|
||||
violations = get_violations(cloudfront_distribution_resource)
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
// Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# CloudTrail Server-Side Encryption Enabled
|
||||
|
||||
## Source Sentinel Policy
|
||||
`cloudtrail-server-side-encryption-enabled.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Good`
|
||||
|
||||
## Why this is Good
|
||||
The Sentinel policy is config-oriented and checks whether `kms_key_id` is present as a configured value. tfpolicy can preserve the same enforcement intent by validating the planned end-state value for `attrs.kms_key_id`.
|
||||
|
||||
## Key translation notes
|
||||
- `tfconfig/v2` config inspection becomes a planned-value check in tfpolicy
|
||||
- The converted policy focuses on whether `kms_key_id` is ultimately present, not whether it originated as a constant in the config
|
||||
|
||||
## Limitations encountered
|
||||
The tfpolicy version does not preserve the config-level distinction between explicit constant values and other configuration forms. It validates the final planned attribute value instead.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Converted from HashiCorp PCI DSS Sentinel example: cloudtrail-server-side-encryption-enabled.sentinel
|
||||
# Conversion quality: Good
|
||||
|
||||
resource_policy "aws_cloudtrail" "cloudtrail_server_side_encryption_enabled" {
|
||||
locals {
|
||||
kms_key_id = core::try(attrs.kms_key_id, "")
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.kms_key_id != ""
|
||||
error_message = "CloudTrail resources must set kms_key_id for server-side encryption"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# This policy requires that resources of type `aws_cloudtrail` have server-side encryption enabled.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
|
||||
import "tfconfig/v2" as tfconfig
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Constants
|
||||
|
||||
const = {
|
||||
"resource_aws_cloudtrail": "aws_cloudtrail",
|
||||
"policy_name": "cloudtrail-server-side-encryption-enabled",
|
||||
"message": "Attribute 'kms_key_id' must be present for 'aws_cloudtrail' resources. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/cloudtrail-controls.html#cloudtrail-2 for more details.",
|
||||
"cloudtrail_attribute_kms_key_id": "kms_key_id",
|
||||
"constant_value": "constant_value",
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
resources = tf.config(tfconfig.resources).type(const.resource_aws_cloudtrail).resources
|
||||
|
||||
violations = collection.reject(resources, func(res) {
|
||||
key_path = "config.kms_key_id"
|
||||
return maps.get(res, key_path, false) is not false and
|
||||
maps.get(res, key_path + "." + const.constant_value, false) is not ""
|
||||
})
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# DMS Endpoint Should Be SSL Configured
|
||||
|
||||
## Source Sentinel Policy
|
||||
`dms-endpoint-should-be-ssl-configured.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Good`
|
||||
|
||||
## Why this converts reasonably well
|
||||
The Sentinel version uses `tfconfig/v2` to accept either a constant value or a reference for `certificate_arn`. tfpolicy cannot inspect Terraform config reference metadata the same way, but it can still validate that the planned `certificate_arn` value is non-empty.
|
||||
|
||||
## Key translation notes
|
||||
- Config-oriented Sentinel checks become an end-state tfpolicy check on `attrs.certificate_arn`
|
||||
- tfpolicy focuses on the resulting planned value instead of whether it came from a literal or a reference
|
||||
|
||||
## Limitations encountered
|
||||
The tfpolicy version does not preserve the source-level distinction between constant values and references. It only checks that the final planned value is present.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Converted from HashiCorp PCI DSS Sentinel example: dms-endpoint-should-be-ssl-configured.sentinel
|
||||
# Conversion quality: Good
|
||||
|
||||
resource_policy "aws_dms_endpoint" "dms_endpoint_should_be_ssl_configured" {
|
||||
locals {
|
||||
certificate_arn = core::try(attrs.certificate_arn, "")
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.certificate_arn != ""
|
||||
error_message = "DMS endpoints should set certificate_arn for SSL configuration"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
# This policy checks if resources of type 'aws_dms_endpoint' have the 'certificate_arn'
|
||||
# shouldn't be empty
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
import "tfconfig/v2" as tfconfig
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Constants
|
||||
const = {
|
||||
"policy_name": "dms-endpoint-should-be-ssl-configured",
|
||||
"message": "Attribute 'certificate_arn' shouldn't be empty for AWS DMS Endpoint. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/dms-controls.html#dms-9 for more details.",
|
||||
"resource_aws_dms_endpoint": "aws_dms_endpoint",
|
||||
}
|
||||
|
||||
# Functions
|
||||
|
||||
get_violations = func(resources) {
|
||||
return collection.reject(resources, func(res) {
|
||||
certificate_arn_values = maps.get(res, "config.certificate_arn", "")
|
||||
if certificate_arn_values is empty {
|
||||
return false
|
||||
}
|
||||
return maps.get(certificate_arn_values, "constant_value", "") is not empty or maps.get(certificate_arn_values, "references", "") is not empty
|
||||
})
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
dms_endpoint_resource = tf.config(tfconfig.resources).type(const.resource_aws_dms_endpoint).resources
|
||||
violations = get_violations(dms_endpoint_resource)
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# DMS Endpoint SSL Mode
|
||||
|
||||
## Source Sentinel Policy
|
||||
`dms-endpoints-should-use-ssl.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Perfect`
|
||||
|
||||
## Why it converts well
|
||||
This policy is a straightforward single-resource attribute check. The Sentinel version iterates over `aws_dms_endpoint` resources and rejects any resource whose `ssl_mode` is not in an allowlist. tfpolicy can express the same intent directly with one `resource_policy`, one allowlist, and one `enforce` block.
|
||||
|
||||
## Key translation notes
|
||||
- Sentinel `collection.reject()` becomes one positive `condition`
|
||||
- `maps.get(res, "values.ssl_mode", null)` becomes `core::try(attrs.ssl_mode, "")`
|
||||
- No cross-resource logic, state inspection, or reference metadata is involved
|
||||
|
||||
## Limitations encountered
|
||||
No significant tfpolicy limitation blocks this conversion.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Converted from HashiCorp PCI DSS Sentinel example: dms-endpoints-should-use-ssl.sentinel
|
||||
# Conversion quality: Perfect
|
||||
|
||||
resource_policy "aws_dms_endpoint" "require_ssl_mode" {
|
||||
locals {
|
||||
ssl_mode = core::try(attrs.ssl_mode, "")
|
||||
valid_ssl_modes = ["require", "verify-ca", "verify-full"]
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = core::contains(local.valid_ssl_modes, local.ssl_mode)
|
||||
error_message = "DMS endpoints must set ssl_mode to one of: require, verify-ca, verify-full"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# This policy requires resources of type `aws_dms_endpoint` have attribute "ssl_mode" set to one of: require, verify-ca, verify-full.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
|
||||
import "tfplan/v2" as tfplan
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Constants
|
||||
|
||||
const = {
|
||||
"policy_name": "dms-ssl-enabled",
|
||||
"message": "Attribute 'ssl_mode' must be set to one of: require, verify-ca, verify-full for 'aws_dms_endpoint' resources. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/dms-controls.html#dms-9 for more details.",
|
||||
"resource_aws_dms_endpoint": "aws_dms_endpoint",
|
||||
"ssl_mode": "ssl_mode",
|
||||
"valid_ssl_modes": ["require", "verify-ca", "verify-full"],
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
resources = tf.plan(tfplan.planned_values.resources).type(const.resource_aws_dms_endpoint).resources
|
||||
violations = collection.reject(resources, func(res) {
|
||||
return maps.get(res, "values." + const.ssl_mode, null) in const.valid_ssl_modes
|
||||
})
|
||||
|
||||
summary = {
|
||||
"policy_name": "dms-ssl-enabled",
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# EC2 Network ACL Should Have Subnet IDs
|
||||
|
||||
## Source Sentinel Policy
|
||||
`ec2-network-acl-should-have-subnet-ids.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Limited`
|
||||
|
||||
## Why this is limited
|
||||
The Sentinel policy uses `tfconfig/v2`, reference metadata, and module-aware address reconstruction to determine whether a network ACL is connected through `aws_network_acl_association`. Current tfpolicy guidance does not expose equivalent reference metadata, so an exact translation is not possible.
|
||||
|
||||
## What the approximation does
|
||||
The tfpolicy version checks either:
|
||||
- `subnet_ids` is present directly on the network ACL, or
|
||||
- a matching `aws_network_acl_association` can be found via `core::getresources()` and a value-based lookup
|
||||
|
||||
## Limitations encountered
|
||||
- This is value matching, not true Terraform graph reasoning
|
||||
- It may behave differently for newly created resources with unresolved values
|
||||
- It does not reproduce the Sentinel policy's module-aware reference reconstruction exactly
|
||||
@@ -0,0 +1,24 @@
|
||||
# Approximation of HashiCorp PCI DSS Sentinel example: ec2-network-acl-should-have-subnet-ids.sentinel
|
||||
# Exact conversion quality: Limited
|
||||
|
||||
locals {
|
||||
all_network_acl_associations = core::getresources("aws_network_acl_association", {})
|
||||
associated_network_acl_ids = {
|
||||
for association in local.all_network_acl_associations :
|
||||
core::try(association.network_acl_id, "") => true
|
||||
}
|
||||
}
|
||||
|
||||
resource_policy "aws_network_acl" "network_acl_should_have_subnet_ids" {
|
||||
locals {
|
||||
subnet_ids = core::try(attrs.subnet_ids, [])
|
||||
has_subnet_ids = core::length(local.subnet_ids) > 0
|
||||
network_acl_id = core::try(attrs.id, "")
|
||||
has_association = core::try(local.associated_network_acl_ids[local.network_acl_id], false)
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.has_subnet_ids || local.has_association
|
||||
error_message = "Network ACLs should define subnet_ids directly or have a matching aws_network_acl_association"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// This policy requires `aws_network_acl` resources to have 'subnet_ids' present.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
// Imports
|
||||
|
||||
import "tfconfig/v2" as tfconfig
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
import "strings"
|
||||
|
||||
// Constants
|
||||
|
||||
const = {
|
||||
"policy_name": "ec2-network-acl-should-have-subnet-ids",
|
||||
"message": "Attribute 'subnet_ids' must be present for 'aws_network_acl' resources or it should include 'subnet_ids' through 'aws_network_acl_association'. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-16 for more details.",
|
||||
"resource_aws_network_acl": "aws_network_acl",
|
||||
"resource_aws_network_acl_association": "aws_network_acl_association",
|
||||
"subnet_ids": "subnet_ids",
|
||||
"constant_value": "constant_value",
|
||||
"module_prefix": "module.",
|
||||
}
|
||||
|
||||
// Functions
|
||||
|
||||
get_violations = func(network_acl_resources, network_acl_association_resources) {
|
||||
return collection.reject(network_acl_resources, func(res) {
|
||||
subnet_id_values = maps.get(res, "config." + const.subnet_ids, [])
|
||||
if (subnet_id_values is empty or subnet_id_values.constant_value is defined) and check_network_acl_association(res.address, network_acl_association_resources) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
check_network_acl_association = func(address, network_acl_association_resources) {
|
||||
if network_acl_association_resources is empty {
|
||||
return true
|
||||
}
|
||||
return collection.find(network_acl_association_resources, func(res) {
|
||||
network_acl_id_reference = get_referenced_resource_address(res, "config.network_acl_id")
|
||||
if network_acl_id_reference is empty {
|
||||
return false
|
||||
}
|
||||
return address is network_acl_id_reference
|
||||
}) is not defined
|
||||
}
|
||||
|
||||
get_referenced_resource_address = func(res, attr) {
|
||||
references_list = maps.get(res, attr, [])
|
||||
if references_list.references is empty {
|
||||
return ""
|
||||
}
|
||||
referenced_address = references_list.references[1]
|
||||
if strings.has_prefix(res.address, const.module_prefix) {
|
||||
referenced_address = res.module_address + "." + referenced_address
|
||||
}
|
||||
return referenced_address
|
||||
}
|
||||
|
||||
// Variables
|
||||
|
||||
config_resources = tf.config(tfconfig.resources)
|
||||
network_acl_resources = config_resources.type(const.resource_aws_network_acl).resources
|
||||
network_acl_association_resources = config_resources.type(const.resource_aws_network_acl_association).resources
|
||||
|
||||
violations = get_violations(network_acl_resources, network_acl_association_resources)
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
// Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# EC2 VPC Default Security Group No Traffic
|
||||
|
||||
## Source Sentinel Policy
|
||||
`ec2-vpc-default-security-group-no-traffic.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Not convertible` as an exact translation
|
||||
|
||||
## What the approximation does
|
||||
The included tfpolicy checks only inline `ingress` and `egress` rules on `aws_default_security_group` resources.
|
||||
|
||||
## Why exact conversion is not possible today
|
||||
The Sentinel policy combines several config-level resource types:
|
||||
- `aws_default_security_group`
|
||||
- `aws_security_group_rule`
|
||||
- `aws_vpc_security_group_ingress_rule`
|
||||
- `aws_vpc_security_group_egress_rule`
|
||||
|
||||
It then uses `tfconfig/v2` reference metadata and regex checks to determine whether those separate rule resources target the default security group of a VPC. Current tfpolicy guidance does not expose equivalent config graph metadata, so it cannot safely reproduce that full relationship-aware behavior.
|
||||
|
||||
## Key limitation
|
||||
This means tfpolicy can approximate the inline-rule case, but it cannot fully enforce the broader Sentinel policy that also reasons over separate security group rule resources attached by reference.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Approximation of HashiCorp PCI DSS Sentinel example: ec2-vpc-default-security-group-no-traffic.sentinel
|
||||
# Exact conversion quality: Not convertible
|
||||
# This tfpolicy only checks inline ingress/egress on aws_default_security_group resources.
|
||||
|
||||
resource_policy "aws_default_security_group" "ec2_vpc_default_security_group_no_traffic" {
|
||||
locals {
|
||||
ingress_rules = core::try(attrs.ingress, [])
|
||||
egress_rules = core::try(attrs.egress, [])
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = core::length(local.ingress_rules) == 0
|
||||
error_message = "Default security groups should not allow inline ingress traffic"
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = core::length(local.egress_rules) == 0
|
||||
error_message = "Default security groups should not allow inline egress traffic"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# This policy requires resources of type `aws_vpc` to have no traffic for default security group.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
|
||||
import "tfconfig/v2" as tfconfig
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Constants
|
||||
|
||||
const = {
|
||||
"message": "VPC default security group should not allow inbound and outbound traffic. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-2 for more details.",
|
||||
"policy_name": "ec2-vpc-default-security-group-no-traffic",
|
||||
"config": "config",
|
||||
"security_group_id": "security_group_id",
|
||||
"references": "references",
|
||||
"constant_value": "constant_value",
|
||||
"resource_aws_default_security_group": "aws_default_security_group",
|
||||
"ingress": "ingress",
|
||||
"egress": "egress",
|
||||
"resource_aws_vpc": "aws_vpc",
|
||||
"resource_aws_default_vpc": "aws_default_vpc",
|
||||
"resource_aws_security_group_rule": "aws_security_group_rule",
|
||||
"resource_aws_vpc_security_group_ingress_rule": "aws_vpc_security_group_ingress_rule",
|
||||
"resource_aws_vpc_security_group_egress_rule": "aws_vpc_security_group_egress_rule",
|
||||
}
|
||||
|
||||
# Functions
|
||||
|
||||
is_default_security_group_of_vpc = func(reference) {
|
||||
return reference matches "aws_default_security_group.(.*).id" or
|
||||
reference matches "aws_vpc.(.*).default_security_group_id$" or
|
||||
reference matches "aws_default_vpc.(.*).default_security_group_id$"
|
||||
}
|
||||
|
||||
filter_security_group_rule_violations = func(sg_rule_resources) {
|
||||
return collection.reject(sg_rule_resources, func(r) {
|
||||
key = "config.security_group_id.references"
|
||||
val = maps.get(r, key, undefined)
|
||||
return !(val is defined and length(val) > 0 and is_default_security_group_of_vpc(val[0]))
|
||||
})
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
config_resources = tf.config(tfconfig.resources)
|
||||
|
||||
default_security_group_resources = config_resources.type(const.resource_aws_default_security_group).resources
|
||||
|
||||
violations = []
|
||||
|
||||
violations += collection.reject(default_security_group_resources, func(r) {
|
||||
ingress_key = const.config + "." + const.ingress + "." + const.constant_value
|
||||
egress_key = const.config + "." + const.egress + "." + const.constant_value
|
||||
ingress_key_val = maps.get(r, ingress_key, undefined)
|
||||
egress_key_val = maps.get(r, egress_key, undefined)
|
||||
return !((ingress_key_val is defined and length(ingress_key_val) > 0) or
|
||||
(egress_key_val is defined and length(egress_key_val) > 0))
|
||||
})
|
||||
|
||||
aws_security_group_rule_resources = config_resources.type(const.resource_aws_security_group_rule).resources
|
||||
violations += filter_security_group_rule_violations(aws_security_group_rule_resources)
|
||||
|
||||
aws_security_group_ingress_rule_resources = config_resources.type(const.resource_aws_vpc_security_group_ingress_rule).resources
|
||||
violations += filter_security_group_rule_violations(aws_security_group_ingress_rule_resources)
|
||||
|
||||
aws_security_group_egress_rule_resources = config_resources.type(const.resource_aws_vpc_security_group_egress_rule).resources
|
||||
violations += filter_security_group_rule_violations(aws_security_group_egress_rule_resources)
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# EFS Access Point Should Enforce User Identity
|
||||
|
||||
## Source Sentinel Policy
|
||||
`efs-access-point-should-enforce-user-identity.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Perfect`
|
||||
|
||||
## Why it converts well
|
||||
This is a simple presence check on a single planned resource type. The Sentinel policy rejects `aws_efs_access_point` resources that do not define `posix_user`, and tfpolicy can express that directly with one `resource_policy` and one `enforce` block.
|
||||
|
||||
## Key translation notes
|
||||
- `maps.get(res.values, "posix_user", {}) is not empty` becomes `core::try(attrs.posix_user, null) != null`
|
||||
- No cross-resource reasoning or reference metadata is required
|
||||
|
||||
## Limitations encountered
|
||||
No significant tfpolicy limitation blocks this conversion.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Converted from HashiCorp PCI DSS Sentinel example: efs-access-point-should-enforce-user-identity.sentinel
|
||||
# Conversion quality: Perfect
|
||||
|
||||
resource_policy "aws_efs_access_point" "efs_access_point_should_enforce_user_identity" {
|
||||
enforce {
|
||||
condition = core::try(attrs.posix_user, null) != null
|
||||
error_message = "EFS access points must define posix_user"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# This policy requires resources of type `aws_efs_access_point` have attribute `posix_user` should be defined.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
|
||||
import "tfplan/v2" as tfplan
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Constants
|
||||
|
||||
const = {
|
||||
"policy_name": "efs-access-point-should-enforce-user-identity",
|
||||
"message": "Attribute 'posix_user' should be defined for 'aws_efs_access_point' resources. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/efs-controls.html#efs-4 for more details.",
|
||||
"resource_aws_efs_access_point": "aws_efs_access_point",
|
||||
"posix_user": "posix_user",
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
resources = tf.plan(tfplan.planned_values.resources).type(const.resource_aws_efs_access_point).resources
|
||||
|
||||
violations = collection.reject(resources, func(res) {
|
||||
return maps.get(res.values, const.posix_user, {}) is not empty
|
||||
})
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# ElastiCache Redis Replication Group Encryption at Transit Enabled
|
||||
|
||||
## Source Sentinel Policy
|
||||
`elasticache-redis-replication-group-encryption-at-transit-enabled.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Perfect`
|
||||
|
||||
## Why it converts well
|
||||
This is a direct boolean check on a single planned resource type. The Sentinel logic checks whether `transit_encryption_enabled` is true on `aws_elasticache_replication_group`, and tfpolicy can express the same rule directly.
|
||||
|
||||
## Key translation notes
|
||||
- `maps.get(res, "values.transit_encryption_enabled", ...)` becomes `core::try(attrs.transit_encryption_enabled, false)`
|
||||
- No resource graph traversal, config metadata, or cross-resource matching is required
|
||||
|
||||
## Limitations encountered
|
||||
No significant tfpolicy limitation blocks this conversion.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Converted from HashiCorp PCI DSS Sentinel example: elasticache-redis-replication-group-encryption-at-transit-enabled.sentinel
|
||||
# Conversion quality: Perfect
|
||||
|
||||
resource_policy "aws_elasticache_replication_group" "elasticache_redis_replication_group_encryption_at_transit_enabled" {
|
||||
enforce {
|
||||
condition = core::try(attrs.transit_encryption_enabled, false) == true
|
||||
error_message = "ElastiCache replication groups must enable transit_encryption_enabled"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# This policy requires that the `transit_encryption_enabled` attribute of the `aws_elasticache_replication_group` resource is true.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
|
||||
import "tfplan/v2" as tfplan
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Constants
|
||||
const = {
|
||||
"policy_name": "elasticache-redis-replication-group-encryption-at-rest-enabled",
|
||||
"resource_aws_elasticache_replication_group": "aws_elasticache_replication_group",
|
||||
}
|
||||
|
||||
# Functions
|
||||
get_violations = func(resources) {
|
||||
return collection.reject(resources, func(res) {
|
||||
key = "values.transit_encryption_enabled"
|
||||
return maps.has(res, key) and maps.get(res, key) is true
|
||||
})
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
elasticache_replication_groups = tf.plan(tfplan.planned_values.resources).type(const.resource_aws_elasticache_replication_group).resources
|
||||
violations = get_violations(elasticache_replication_groups)
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": "Attribute 'transit_encryption_enabled' must be true for 'aws_elasticache_replication_group' resources.Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/elasticache-controls.html#elasticache-5 for more details.",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Elasticsearch Encrypted at Rest
|
||||
|
||||
## Source Sentinel Policy
|
||||
`elasticsearch-encrypted-at-rest.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Good`
|
||||
|
||||
## Why this is Good
|
||||
The original intent maps cleanly to tfpolicy, but the block shape still has to be rewritten in tfpolicy terms using `core::try()` around `encrypt_at_rest[0].enabled`.
|
||||
|
||||
## Key translation notes
|
||||
- Nested map access becomes direct tfpolicy block access
|
||||
- The conversion checks the planned end state of `encrypt_at_rest`
|
||||
- The outcome is preserved even though the syntax changes substantially
|
||||
|
||||
## Limitations encountered
|
||||
This depends on the provider exposing `encrypt_at_rest` in the expected block/list structure. As with other tfpolicy policies, raw provider schema shape matters.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Converted from HashiCorp PCI DSS Sentinel example: elasticsearch-encrypted-at-rest.sentinel
|
||||
# Conversion quality: Good
|
||||
|
||||
resource_policy "aws_elasticsearch_domain" "elasticsearch_encrypted_at_rest" {
|
||||
locals {
|
||||
encrypt_at_rest = core::try(attrs.encrypt_at_rest, [])
|
||||
encryption_enabled = core::try(local.encrypt_at_rest[0].enabled, false)
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.encryption_enabled == true
|
||||
error_message = "Elasticsearch domains must enable encrypt_at_rest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# This policy requires resources of type `aws_elasticsearch_domain` have the `encrypt_at_rest` should have 'enabled' attribute set to `true`.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Import
|
||||
|
||||
import "tfplan/v2" as tfplan
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Constants
|
||||
const = {
|
||||
"policy_name": "elasticsearch-encrypted-at-rest",
|
||||
"message": "Attribute 'enabled' must be set to true for the attribute 'encrypt_at_rest' for 'aws_elasticsearch_domain' resources. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/es-controls.html#es-1 for more details.",
|
||||
"resource_aws_elasticsearch_domain": "aws_elasticsearch_domain",
|
||||
}
|
||||
|
||||
# Functions
|
||||
|
||||
get_violations = func(resources) {
|
||||
return collection.reject(resources, func(res) {
|
||||
encrypt_at_rest_values = maps.get(res, "values.encrypt_at_rest", [])
|
||||
return encrypt_at_rest_values is not empty and encrypt_at_rest_values[0].enabled is true
|
||||
})
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
elasticsearch_resources = tf.plan(tfplan.planned_values.resources).type(const.resource_aws_elasticsearch_domain).resources
|
||||
violations = get_violations(elasticsearch_resources)
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Elasticsearch HTTPS Required
|
||||
|
||||
## Source Sentinel Policy
|
||||
`elasticsearch-https-required.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Good`
|
||||
|
||||
## Why it is not labeled Perfect
|
||||
The enforcement intent is preserved, but the structure changes more noticeably than in a simple attribute check. The Sentinel version uses helper functions plus nested map lookups. The tfpolicy version rewrites that logic into direct block access with `core::try()` and separate `enforce` blocks.
|
||||
|
||||
## Key translation notes
|
||||
- Nested `maps.get()` calls become `core::try(local.endpoint_options[0]....)`
|
||||
- One compound Sentinel predicate becomes multiple focused `enforce` blocks
|
||||
- The end-state requirement is preserved clearly in tfpolicy
|
||||
|
||||
## Limitations encountered
|
||||
This conversion depends on provider schema shape for `domain_endpoint_options`. As with other tfpolicy policies, block/list/set handling must match the exposed schema exactly.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Converted from HashiCorp PCI DSS Sentinel example: elasticsearch-https-required.sentinel
|
||||
# Conversion quality: Good
|
||||
|
||||
resource_policy "aws_elasticsearch_domain" "https_required" {
|
||||
locals {
|
||||
endpoint_options = core::try(attrs.domain_endpoint_options, [])
|
||||
endpoint_options_present = core::length(local.endpoint_options) > 0
|
||||
enforce_https = core::try(local.endpoint_options[0].enforce_https, false)
|
||||
tls_security_policy = core::try(local.endpoint_options[0].tls_security_policy, "")
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.endpoint_options_present
|
||||
error_message = "Elasticsearch domains must define domain_endpoint_options"
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.enforce_https == true
|
||||
error_message = "Elasticsearch domains must set domain_endpoint_options.enforce_https = true"
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.tls_security_policy == "Policy-Min-TLS-1-2-PFS-2023-10"
|
||||
error_message = "Elasticsearch domains must use tls_security_policy 'Policy-Min-TLS-1-2-PFS-2023-10'"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# This policy requires resources of type `aws_elasticsearch_domain` have the `tls_security_policy` set to latest policy that is 'Policy-Min-TLS-1-2-PFS-2023-10' and 'enforce_https' set to true for `domain_endpoint_options` attribute.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Import
|
||||
|
||||
import "tfplan/v2" as tfplan
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Params
|
||||
param master_count_value default 3
|
||||
|
||||
# Constants
|
||||
const = {
|
||||
"policy_name": "elasticsearch-https-required",
|
||||
"message": "Attribute 'tls_security_policy' must be set to latest policy that is 'Policy-Min-TLS-1-2-PFS-2023-10' and 'enforce_https' set to true for the attribute 'domain_endpoint_options' for 'aws_elasticsearch_domain' resources. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/es-controls.html#es-8 for more details.",
|
||||
"resource_aws_elasticsearch_domain": "aws_elasticsearch_domain",
|
||||
"enforce_https": "enforce_https",
|
||||
"tls_security_policy": "tls_security_policy",
|
||||
"allowed_tls_latest_policy": "Policy-Min-TLS-1-2-PFS-2023-10",
|
||||
}
|
||||
|
||||
# Functions
|
||||
|
||||
get_violations = func(resources) {
|
||||
return collection.reject(resources, func(res) {
|
||||
domain_endpoint_options_values = maps.get(res, "values.domain_endpoint_options", [])
|
||||
if domain_endpoint_options_values is empty {
|
||||
return false
|
||||
}
|
||||
tls_security_policy_value = maps.get(domain_endpoint_options_values[0], const.tls_security_policy, null)
|
||||
enforce_https_value = maps.get(domain_endpoint_options_values[0], const.enforce_https, true)
|
||||
if tls_security_policy_value is null {
|
||||
return false
|
||||
}
|
||||
return enforce_https_value is true and tls_security_policy_value == const.allowed_tls_latest_policy
|
||||
})
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
elasticsearch_resources = tf.plan(tfplan.planned_values.resources).type(const.resource_aws_elasticsearch_domain).resources
|
||||
violations = get_violations(elasticsearch_resources)
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Elasticsearch In VPC Only
|
||||
|
||||
## Source Sentinel Policy
|
||||
`elasticsearch-in-vpc-only.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Limited`
|
||||
|
||||
## Why this is limited
|
||||
The Sentinel policy is config-oriented and accepts either constant subnet IDs or references inside `vpc_options.subnet_ids`. tfpolicy does not expose the same config-level `constant_value` and `references` metadata, so it cannot preserve that distinction exactly.
|
||||
|
||||
## What the tfpolicy approximation does
|
||||
The tfpolicy version checks the planned end state and requires `vpc_options[0].subnet_ids` to contain one or more values.
|
||||
|
||||
## Limitations encountered
|
||||
- It validates the resulting planned subnet IDs, not whether they originated from constants vs references
|
||||
- It assumes the provider exposes `vpc_options` and `subnet_ids` in the expected schema shape
|
||||
- It is a useful enforcement approximation, but not a one-to-one tfconfig translation
|
||||
@@ -0,0 +1,14 @@
|
||||
# Approximation of HashiCorp PCI DSS Sentinel example: elasticsearch-in-vpc-only.sentinel
|
||||
# Exact conversion quality: Limited
|
||||
|
||||
resource_policy "aws_elasticsearch_domain" "elasticsearch_in_vpc_only" {
|
||||
locals {
|
||||
vpc_options = core::try(attrs.vpc_options, [])
|
||||
subnet_ids = core::try(local.vpc_options[0].subnet_ids, [])
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = core::length(local.subnet_ids) > 0
|
||||
error_message = "Elasticsearch domains should define one or more subnet_ids in vpc_options"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# This policy requires resources of type `aws_elasticsearch_domain` have the `subnet_ids` should not be empty inside 'vpc_options'.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Import
|
||||
|
||||
import "tfconfig/v2" as tfconfig
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Constants
|
||||
const = {
|
||||
"policy_name": "elasticsearch-in-vpc-only",
|
||||
"message": "Attribute 'subnet_ids' should not be empty for the attribute 'vpc_options' for 'aws_elasticsearch_domain' resources. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/es-controls.html#es-2 for more details.",
|
||||
"resource_aws_elasticsearch_domain": "aws_elasticsearch_domain",
|
||||
"subnet_ids": "subnet_ids",
|
||||
"constant_value": "constant_value",
|
||||
"references": "references",
|
||||
}
|
||||
|
||||
# Functions
|
||||
|
||||
get_violations = func(resources) {
|
||||
return collection.reject(resources, func(res) {
|
||||
vpc_options_values = maps.get(res, "config.vpc_options", [])
|
||||
if vpc_options_values is empty {
|
||||
return false
|
||||
}
|
||||
subnet_ids_values = maps.get(vpc_options_values[0], const.subnet_ids, [])
|
||||
if subnet_ids_values is empty {
|
||||
return false
|
||||
}
|
||||
return maps.get(subnet_ids_values, const.constant_value, []) is not empty or maps.get(subnet_ids_values, const.references, []) is not empty
|
||||
})
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
elasticsearch_resources = tf.config(tfconfig.resources).type(const.resource_aws_elasticsearch_domain).resources
|
||||
violations = get_violations(elasticsearch_resources)
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# EventBridge Bus Must Have Attached Policy
|
||||
|
||||
## Source Sentinel Policy
|
||||
`eventbridge-custom-event-bus-should-have-attached-policy.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Limited`
|
||||
|
||||
## Why this is only a partial conversion
|
||||
The Sentinel version can compare planned event bus resources against planned policy resources cleanly inside its own collection-processing model. tfpolicy can approximate that by using `core::getresources()` and matching on `event_bus_name`, but this is not a full graph-aware translation.
|
||||
|
||||
## Key translation notes
|
||||
- Related resources are discovered with `core::getresources("aws_cloudwatch_event_bus_policy", {})`
|
||||
- Matching is done by explicit value (`event_bus_name`) rather than graph/reference semantics
|
||||
- A top-level lookup map keeps the tfpolicy example readable and performant
|
||||
|
||||
## Limitations encountered
|
||||
- This approach relies on resolved attribute values, not reference metadata
|
||||
- New resources with unresolved references may not match reliably on initial creation
|
||||
- `core::getresources()` is useful for scoped lookups but is not a full replacement for Sentinel graph traversal
|
||||
@@ -0,0 +1,22 @@
|
||||
# Converted from HashiCorp PCI DSS Sentinel example: eventbridge-custom-event-bus-should-have-attached-policy.sentinel
|
||||
# Conversion quality: Limited
|
||||
|
||||
locals {
|
||||
all_event_bus_policies = core::getresources("aws_cloudwatch_event_bus_policy", {})
|
||||
event_bus_policy_map = {
|
||||
for policy in local.all_event_bus_policies :
|
||||
policy.event_bus_name => true
|
||||
}
|
||||
}
|
||||
|
||||
resource_policy "aws_cloudwatch_event_bus" "require_attached_policy" {
|
||||
locals {
|
||||
bus_name = core::try(attrs.name, "")
|
||||
has_attached_policy = core::try(local.event_bus_policy_map[local.bus_name], false)
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.has_attached_policy
|
||||
error_message = "EventBridge buses must have a matching aws_cloudwatch_event_bus_policy resource"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
# This policy requires `aws_cloudwatch_event_bus` resources to be attached to a policy.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
|
||||
import "tfplan/v2" as tfplan
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
import "strings"
|
||||
|
||||
# Constants
|
||||
|
||||
const = {
|
||||
"policy_name": "eventbridge-custom-event-bus-should-have-attached-policy",
|
||||
"message": "Policy should be attached for 'aws_cloudwatch_event_bus' resource. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/eventbridge-controls.html#eventbridge-3 for more details.",
|
||||
"resource_aws_cloudwatch_event_bus_policy": "aws_cloudwatch_event_bus_policy",
|
||||
"resource_aws_cloudwatch_event_bus": "aws_cloudwatch_event_bus",
|
||||
"event_bus_name": "event_bus_name",
|
||||
"name": "name",
|
||||
}
|
||||
|
||||
# Functions
|
||||
|
||||
get_bus_name_complaint = func(resources) {
|
||||
return collection.reject(resources, func(res) {
|
||||
bus_name_values = maps.get(res, "values." + const.event_bus_name, {})
|
||||
if bus_name_values is empty {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
plan_resources = tf.plan(tfplan.planned_values.resources)
|
||||
event_bus_policy_resources = plan_resources.type(const.resource_aws_cloudwatch_event_bus_policy).resources
|
||||
event_bus_resources = plan_resources.type(const.resource_aws_cloudwatch_event_bus).resources
|
||||
|
||||
event_bus_complaint = get_bus_name_complaint(event_bus_policy_resources)
|
||||
if event_bus_complaint is not defined {
|
||||
violations = []
|
||||
}
|
||||
|
||||
event_bus_addresses = map event_bus_complaint as _, res {
|
||||
maps.get(res, "values." + const.event_bus_name, {})
|
||||
}
|
||||
|
||||
violations = filter event_bus_resources as _, res {
|
||||
maps.get(res, "values." + const.name, {}) not in event_bus_addresses
|
||||
}
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# S3 Block Public Access Bucket Level
|
||||
|
||||
## Source Sentinel Policy
|
||||
`s3-block-public-access-bucket-level.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Not convertible` as an exact translation
|
||||
|
||||
## What the approximation does
|
||||
The tfpolicy approximation checks whether an `aws_s3_bucket` has a matching `aws_s3_bucket_public_access_block` resource and whether all four public access settings are enabled.
|
||||
|
||||
## Why exact conversion is not possible today
|
||||
The Sentinel policy combines:
|
||||
- `tfconfig/v2`
|
||||
- `tfconfig-functions`
|
||||
- plan-time variable resolution
|
||||
- config reference metadata
|
||||
- module-aware address reconstruction
|
||||
|
||||
Current tfpolicy guidance does not expose that full config-analysis surface. In particular, tfpolicy cannot safely reproduce the Sentinel behavior that inspects variable references and configuration graph relationships before values are fully materialized.
|
||||
|
||||
## Limitations encountered
|
||||
- The approximation relies on resolved values via `core::getresources()`
|
||||
- It cannot reproduce variable-reference evaluation from the Sentinel policy
|
||||
- It may differ from Sentinel on first creation or heavily parameterized module usage
|
||||
@@ -0,0 +1,27 @@
|
||||
# Approximation of HashiCorp PCI DSS Sentinel example: s3-block-public-access-bucket-level.sentinel
|
||||
# Exact conversion quality: Not convertible
|
||||
|
||||
locals {
|
||||
all_public_access_blocks = core::getresources("aws_s3_bucket_public_access_block", {})
|
||||
compliant_public_access_blocks = {
|
||||
for block in local.all_public_access_blocks :
|
||||
core::try(block.bucket, "") => (
|
||||
core::try(block.ignore_public_acls, false) == true &&
|
||||
core::try(block.restrict_public_buckets, false) == true &&
|
||||
core::try(block.block_public_acls, false) == true &&
|
||||
core::try(block.block_public_policy, false) == true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
resource_policy "aws_s3_bucket" "s3_block_public_access_bucket_level" {
|
||||
locals {
|
||||
bucket_name = core::try(attrs.bucket, "")
|
||||
block_is_compliant = core::try(local.compliant_public_access_blocks[local.bucket_name], false)
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.block_is_compliant
|
||||
error_message = "S3 buckets should have a matching aws_s3_bucket_public_access_block with all four public access settings enabled"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
# This policy verifies if the attributes of the 'aws_s3_bucket_public_access_block'
|
||||
# resource (if present) block public access of an S3 general purpose bucket.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
|
||||
import "tfplan/v2" as plan
|
||||
import "tfplan-functions" as tfplan
|
||||
import "tfconfig-functions" as tfconfig
|
||||
import "tfconfig/v2" as config
|
||||
import "tfresources" as tf
|
||||
import "collection/maps" as maps
|
||||
import "report" as report
|
||||
import "strings"
|
||||
|
||||
# Constants
|
||||
const = {
|
||||
"policy_name": "s3-block-public-access-bucket-level",
|
||||
"module_address": "module_address",
|
||||
"address": "address",
|
||||
"message": "Bucket level Amazon S3 block public access settings are not compliant. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-8 for more details.",
|
||||
"resource_aws_s3_bucket": "aws_s3_bucket",
|
||||
"module_prefix": "module.",
|
||||
"resource_aws_s3_bucket_public_access_block": "aws_s3_bucket_public_access_block",
|
||||
"public_access_block_settings": ["ignore_public_acls", "restrict_public_buckets", "block_public_acls", "block_public_policy"],
|
||||
}
|
||||
|
||||
# Functions
|
||||
|
||||
is_public_access_setting_enabled = func(config, setting) {
|
||||
const_val = maps.get(maps.get(config, setting, {}), "constant_value")
|
||||
if const_val is defined {
|
||||
return const_val is true
|
||||
}
|
||||
references = maps.get(maps.get(config, setting, {}), "references")
|
||||
if references is defined and tfconfig.is_variable_reference(references[0]) {
|
||||
return tfplan.get_variable_value(tfconfig.parse_variable_name_from_reference(references[0])) is true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
is_block_public_access_settings_compliant = func(config) {
|
||||
return all const.public_access_block_settings as _, setting {
|
||||
is_public_access_setting_enabled(config, setting)
|
||||
}
|
||||
}
|
||||
|
||||
# Prefixes the referenced s3 bucket's address with
|
||||
# the module address. This is done because resource
|
||||
# addresses comprise of module addresses
|
||||
sanitize_referenced_s3_bucket_address = func(res) {
|
||||
module_addr = res[const.module_address]
|
||||
if res.config.bucket.constant_value is defined {
|
||||
return ""
|
||||
}
|
||||
|
||||
bucket_reference = res.config.bucket.references[1]
|
||||
# Check for root module
|
||||
if not strings.has_prefix(res[const.address], const.module_prefix) {
|
||||
return bucket_reference
|
||||
}
|
||||
|
||||
return module_addr + "." + bucket_reference
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
config_resources = tf.config(config.resources)
|
||||
|
||||
compliant_public_access_block_resources = filter config_resources.type(const.resource_aws_s3_bucket_public_access_block).resources as _, res {
|
||||
is_block_public_access_settings_compliant(res.config)
|
||||
}
|
||||
|
||||
s3_bucket_addresses = map compliant_public_access_block_resources as _, res {
|
||||
sanitize_referenced_s3_bucket_address(res)
|
||||
}
|
||||
|
||||
violations = filter config_resources.type(const.resource_aws_s3_bucket).resources as _, res {
|
||||
res.address not in s3_bucket_addresses
|
||||
}
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# S3 Bucket Should Have Object Lock Enabled
|
||||
|
||||
## Source Sentinel Policy
|
||||
`s3-bucket-should-have-object-lock-enabled.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Limited`
|
||||
|
||||
## Why this is limited
|
||||
The Sentinel policy uses `tfconfig/v2` plus reference metadata to trace `aws_s3_bucket_object_lock_configuration` resources back to their `aws_s3_bucket` resources, including module-aware address reconstruction. tfpolicy does not expose equivalent config graph metadata.
|
||||
|
||||
## What the tfpolicy approximation does
|
||||
The tfpolicy version uses `core::getresources()` to find `aws_s3_bucket_object_lock_configuration` resources, then matches them to buckets by the resolved `bucket` value and checks the retention mode.
|
||||
|
||||
## Limitations encountered
|
||||
- Matching depends on resolved values, not reference metadata
|
||||
- Initial creation with unresolved bucket references may not match reliably
|
||||
- The approximation checks the end-state relationship but cannot reproduce the Sentinel config-graph logic exactly
|
||||
@@ -0,0 +1,23 @@
|
||||
# Approximation of HashiCorp PCI DSS Sentinel example: s3-bucket-should-have-object-lock-enabled.sentinel
|
||||
# Exact conversion quality: Limited
|
||||
|
||||
locals {
|
||||
all_object_lock_configs = core::getresources("aws_s3_bucket_object_lock_configuration", {})
|
||||
object_lock_bucket_map = {
|
||||
for config in local.all_object_lock_configs :
|
||||
core::try(config.bucket, "") => core::try(config.rule[0].default_retention[0].mode, "")
|
||||
}
|
||||
}
|
||||
|
||||
resource_policy "aws_s3_bucket" "s3_bucket_should_have_object_lock_enabled" {
|
||||
locals {
|
||||
bucket_name = core::try(attrs.bucket, "")
|
||||
retention_mode = core::try(local.object_lock_bucket_map[local.bucket_name], "")
|
||||
object_lock_enabled = core::contains(["GOVERNANCE", "COMPLIANCE"], local.retention_mode)
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.object_lock_enabled
|
||||
error_message = "S3 buckets should have object lock enabled with default retention mode GOVERNANCE or COMPLIANCE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# S3 Buckets should have object lock enabled
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
|
||||
import "tfconfig/v2" as tfconfig
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
import "strings"
|
||||
import "types"
|
||||
|
||||
# Params
|
||||
|
||||
param valid_mode default ["GOVERNANCE", "COMPLIANCE"]
|
||||
|
||||
# Constants
|
||||
|
||||
const = {
|
||||
"policy_name": "s3-bucket-should-have-object-lock-enabled",
|
||||
"message": "S3 Buckets should have object lock enabled. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-15 for more details.",
|
||||
"resource_aws_s3_bucket": "aws_s3_bucket",
|
||||
"resource_aws_s3_bucket_object_lock_configuration": "aws_s3_bucket_object_lock_configuration",
|
||||
"address": "address",
|
||||
"module_address": "module_address",
|
||||
"module_prefix": "module.",
|
||||
"rule": "rule",
|
||||
"default_retention": "default_retention",
|
||||
"mode": "mode",
|
||||
}
|
||||
|
||||
# Functions
|
||||
|
||||
# Prefixes the referenced S3 Bucket's address with
|
||||
# the module address. This is done because resource
|
||||
# addresses comprise of module addresses
|
||||
sanitize_compliant_s3_bucket_address = func(res) {
|
||||
module_addr = res[const.module_address]
|
||||
if res.config.bucket.constant_value is defined {
|
||||
return ""
|
||||
}
|
||||
rule_block = maps.get(res.config, const.rule, [])
|
||||
if rule_block is empty {
|
||||
return ""
|
||||
}
|
||||
|
||||
default_retention = rule_block[0].default_retention[0]
|
||||
if default_retention is empty {
|
||||
return ""
|
||||
}
|
||||
|
||||
mode = maps.get(default_retention, const.mode, "").constant_value
|
||||
if mode is empty or mode not in valid_mode {
|
||||
return ""
|
||||
}
|
||||
|
||||
s3_bucket_reference = res.config.bucket.references[1]
|
||||
# Check for root module
|
||||
if not strings.has_prefix(res[const.address], const.module_prefix) {
|
||||
return s3_bucket_reference
|
||||
}
|
||||
|
||||
return module_addr + "." + s3_bucket_reference
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
config_resources = tf.config(tfconfig.resources)
|
||||
bucket_resources = config_resources.type(const.resource_aws_s3_bucket).resources
|
||||
bucket_object_lock_resources = config_resources.type(const.resource_aws_s3_bucket_object_lock_configuration).resources
|
||||
|
||||
# Get S3 Bucket addresses that have object lock enabled
|
||||
s3_bucket_addresses_with_object_lock = map bucket_object_lock_resources as _, res {
|
||||
sanitize_compliant_s3_bucket_address(res)
|
||||
}
|
||||
|
||||
# Find violations: S3 Buckets that have policy violations
|
||||
violations = filter bucket_resources as _, res {
|
||||
res.address not in s3_bucket_addresses_with_object_lock
|
||||
}
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Secrets Manager Auto Rotation Enabled Check
|
||||
|
||||
## Source Sentinel Policy
|
||||
`secretsmanager-auto-rotation-enabled-check.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Limited`
|
||||
|
||||
## Why this is limited
|
||||
The Sentinel policy uses `tfconfig/v2` reference metadata to determine whether each `aws_secretsmanager_secret` is connected to an `aws_secretsmanager_secret_rotation` resource through `config.secret_id`. Current tfpolicy guidance does not expose equivalent config-level reference metadata.
|
||||
|
||||
## What the tfpolicy approximation does
|
||||
The tfpolicy version uses `core::getresources()` to collect `aws_secretsmanager_secret_rotation` resources and matches them to secrets by planned `secret_id` / `id` values.
|
||||
|
||||
## Limitations encountered
|
||||
- This is value matching, not true Terraform graph reasoning
|
||||
- It may fail or behave differently when secret identifiers are not resolved yet during creation
|
||||
- It does not preserve Sentinel's module-aware reference reconstruction exactly
|
||||
@@ -0,0 +1,22 @@
|
||||
# Approximation of HashiCorp PCI DSS Sentinel example: secretsmanager-auto-rotation-enabled-check.sentinel
|
||||
# Exact conversion quality: Limited
|
||||
|
||||
locals {
|
||||
all_secret_rotations = core::getresources("aws_secretsmanager_secret_rotation", {})
|
||||
rotation_secret_ids = {
|
||||
for rotation in local.all_secret_rotations :
|
||||
core::try(rotation.secret_id, "") => true
|
||||
}
|
||||
}
|
||||
|
||||
resource_policy "aws_secretsmanager_secret" "secretsmanager_auto_rotation_enabled_check" {
|
||||
locals {
|
||||
secret_id = core::try(attrs.id, "")
|
||||
has_rotation = core::try(local.rotation_secret_ids[local.secret_id], false)
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = local.has_rotation
|
||||
error_message = "Secrets Manager secrets should have a matching aws_secretsmanager_secret_rotation resource"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# This policy requires resources of type `aws_secretsmanager_secret` should be configured for automatic rotation.
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
|
||||
import "tfconfig/v2" as tfconfig
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
import "strings"
|
||||
|
||||
# Constants
|
||||
|
||||
const = {
|
||||
"policy_name": "secretsmanager-auto-rotation-enabled-check",
|
||||
"message": "Secrets Manager secrets should be configured for automatic rotation. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/secretsmanager-controls.html#secretsmanager-1 for more details.",
|
||||
"resource_aws_secretsmanager_secret": "aws_secretsmanager_secret",
|
||||
"resource_aws_secretsmanager_secret_rotation": "aws_secretsmanager_secret_rotation",
|
||||
"kms_master_key_id": "kms_master_key_id",
|
||||
"sqs_managed_sse_enabled": "sqs_managed_sse_enabled",
|
||||
"module_prefix": "module.",
|
||||
}
|
||||
|
||||
# Functions
|
||||
|
||||
get_referenced_resource_address = func(res, attr) {
|
||||
references_list = maps.get(res, attr, [])
|
||||
if references_list.references is empty or references_list.references is not defined {
|
||||
return ""
|
||||
}
|
||||
referenced_address = references_list.references[1]
|
||||
if strings.has_prefix(res.address, const.module_prefix) {
|
||||
referenced_address = res.module_address + "." + referenced_address
|
||||
}
|
||||
return referenced_address
|
||||
}
|
||||
|
||||
# Variables
|
||||
|
||||
secret_resources = tf.config(tfconfig.resources).type(const.resource_aws_secretsmanager_secret).resources
|
||||
secret_rotation_complaint_resources = tf.config(tfconfig.resources).type(const.resource_aws_secretsmanager_secret_rotation).resources
|
||||
|
||||
secret_addresses = map secret_rotation_complaint_resources as _, res {
|
||||
get_referenced_resource_address(res, "config.secret_id")
|
||||
}
|
||||
|
||||
violations = filter secret_resources as _, res {
|
||||
res.address not in secret_addresses
|
||||
}
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Step Functions State Machine Logging Enabled
|
||||
|
||||
## Source Sentinel Policy
|
||||
`step-functions-state-machine-logging-enabled.sentinel`
|
||||
|
||||
## Conversion Quality
|
||||
`Good`
|
||||
|
||||
## Why this is Good
|
||||
This policy is still a single-resource planned-value check, but it relies on a nested block (`logging_configuration`) and an allowlist of valid levels. tfpolicy can express that clearly with `core::try()` and a small local allowlist.
|
||||
|
||||
## Key translation notes
|
||||
- Nested map access becomes direct block access through `attrs.logging_configuration[0].level`
|
||||
- The allowed log levels carry over directly into the tfpolicy version
|
||||
|
||||
## Limitations encountered
|
||||
This relies on the provider exposing `logging_configuration` in the expected block/list shape. Otherwise, the enforcement intent maps cleanly.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Converted from HashiCorp PCI DSS Sentinel example: step-functions-state-machine-logging-enabled.sentinel
|
||||
# Conversion quality: Good
|
||||
|
||||
resource_policy "aws_sfn_state_machine" "step_functions_state_machine_logging_enabled" {
|
||||
locals {
|
||||
logging_configuration = core::try(attrs.logging_configuration, [])
|
||||
log_level = core::try(local.logging_configuration[0].level, "")
|
||||
allowed_levels = ["ALL", "ERROR", "FATAL"]
|
||||
}
|
||||
|
||||
enforce {
|
||||
condition = core::contains(local.allowed_levels, local.log_level)
|
||||
error_message = "Step Functions state machines must set logging_configuration.level to ALL, ERROR, or FATAL"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# This policy requires AWS Step Functions state machines to have logging configuration enabled with level set to "ALL", "ERROR", or "FATAL".
|
||||
|
||||
# Copyright IBM Corp. 2025
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
# Imports
|
||||
import "tfplan/v2" as tfplan
|
||||
import "tfresources" as tf
|
||||
import "report" as report
|
||||
import "collection" as collection
|
||||
import "collection/maps" as maps
|
||||
|
||||
# Constants
|
||||
const = {
|
||||
"policy_name": "sfn-logging-enabled",
|
||||
"message": "AWS Step Functions state machines must have logging enabled with level set to 'ALL', 'ERROR', or 'FATAL'. Refer to https://docs.aws.amazon.com/securityhub/latest/userguide/stepfunctions-controls.html#stepfunctions-1 for more details.",
|
||||
"resource_aws_sfn": "aws_sfn_state_machine",
|
||||
"logging_config": "logging_configuration",
|
||||
"required_log_levels": ["ALL", "ERROR", "FATAL"],
|
||||
}
|
||||
|
||||
# Variables
|
||||
resources = tf.plan(tfplan.planned_values.resources).type(const.resource_aws_sfn).resources
|
||||
|
||||
violations = collection.reject(resources, func(res) {
|
||||
logging_config = maps.get(res, "values." + const.logging_config, null)
|
||||
|
||||
if logging_config is null {
|
||||
return false
|
||||
}
|
||||
log_level = maps.get(logging_config[0], "level", null)
|
||||
if log_level is null {
|
||||
return false
|
||||
}
|
||||
|
||||
return log_level in const.required_log_levels
|
||||
})
|
||||
|
||||
summary = {
|
||||
"policy_name": const.policy_name,
|
||||
"violations": map violations as _, v {
|
||||
{
|
||||
"address": v.address,
|
||||
"module_address": v.module_address,
|
||||
"message": const.message,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Outputs
|
||||
print(report.generate_policy_report(summary))
|
||||
|
||||
# Rules
|
||||
main = rule {
|
||||
violations is empty
|
||||
}
|
||||
1750
.agents/skills/terraform-policy/references/tfpolicy-author.md
Normal file
1750
.agents/skills/terraform-policy/references/tfpolicy-author.md
Normal file
File diff suppressed because it is too large
Load Diff
2111
.agents/skills/terraform-policy/references/tfpolicy-test.md
Normal file
2111
.agents/skills/terraform-policy/references/tfpolicy-test.md
Normal file
File diff suppressed because it is too large
Load Diff
2024
.agents/skills/terraform-policy/references/verified-syntax.md
Normal file
2024
.agents/skills/terraform-policy/references/verified-syntax.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user