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:
2026-08-09 12:17:26 +03:00
commit a6d8aa8b52
160 changed files with 24260 additions and 0 deletions

View 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.

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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'"
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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
}