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