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:
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,41 @@
|
||||
# Terraform Policy Agent Skills
|
||||
|
||||
A family of focused agent skills for working with [Terraform Policy](https://developer.hashicorp.com/terraform/cloud-docs/policy-enforcement) — HCP Terraform's native policy-as-code engine for `.policy.hcl` and `.policytest.hcl` files.
|
||||
|
||||
## Routing
|
||||
|
||||
Pick the skill that matches the user's journey:
|
||||
|
||||
| Journey | Reference |
|
||||
| --- | --- |
|
||||
| Write a new Terraform Policy from an English description | [**tfpolicy-author**](references/tfpolicy-author.md) |
|
||||
| Translate Sentinel (or adjacent OPA/Rego) to Terraform Policy | [**tfpolicy-author**](references/tfpolicy-author.md) |
|
||||
| Write or debug a `.policytest.hcl` test, mock resources, reason about the runner | [**tfpolicy-test**](references/tfpolicy-test.md) |
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
terraform-policy/
|
||||
├── SKILL.md # Router — routes to references below
|
||||
├── references/
|
||||
│ ├── tfpolicy-author.md # Authoring + Sentinel conversion (v0.2.0)
|
||||
│ ├── tfpolicy-test.md # Testing + full testing guide
|
||||
│ └── verified-syntax.md # Shared source-of-truth syntax reference
|
||||
├── examples/
|
||||
│ └── conversion/ # Side-by-side .sentinel / .policy.hcl examples
|
||||
└── evals/
|
||||
├── eval.yaml
|
||||
└── tasks/
|
||||
```
|
||||
|
||||
## Shared reference
|
||||
|
||||
[`references/verified-syntax.md`](references/verified-syntax.md) is the single source of truth for verified Terraform Policy syntax, function names, and runtime limitations. All reference files link to it rather than duplicating facts — when reference content disagrees with this file, the reference wins.
|
||||
|
||||
## Versioning
|
||||
|
||||
Each reference is versioned independently via its `metadata.version` field.
|
||||
|
||||
## License
|
||||
|
||||
MPL-2.0. Copyright IBM Corp. 2026.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: terraform-policy
|
||||
description: "Write, test, or convert Terraform Policy files (.policy.hcl, .policytest.hcl, Sentinel→tfpolicy). Triggers: policy.hcl, policytest, convert sentinel, tfpolicy, write a policy."
|
||||
license: MPL-2.0
|
||||
metadata:
|
||||
copyright: Copyright IBM Corp. 2026
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# terraform-policy
|
||||
|
||||
**UTILITY SKILL** — INVOKES: [tfpolicy-author](references/tfpolicy-author.md) | [tfpolicy-test](references/tfpolicy-test.md)
|
||||
|
||||
## USE FOR:
|
||||
|
||||
- Writing a new `.policy.hcl` policy from a description or requirement
|
||||
- Converting a `.sentinel` policy to Terraform Policy
|
||||
- Writing or debugging a `.policytest.hcl` test file
|
||||
- Migrating a Sentinel policy library to Terraform Policy
|
||||
|
||||
## DO NOT USE FOR:
|
||||
|
||||
- Writing `.tftest.hcl` files for Terraform modules — use `terraform-test`
|
||||
- General Terraform HCL authoring — use `terraform-style-guide`
|
||||
|
||||
## Routing
|
||||
|
||||
| Task | Sub-skill |
|
||||
|------|-----------|
|
||||
| Write or convert a `.policy.hcl` policy | [tfpolicy-author](references/tfpolicy-author.md) |
|
||||
| Write or debug a `.policytest.hcl` test | [tfpolicy-test](references/tfpolicy-test.md) |
|
||||
|
||||
## Examples
|
||||
|
||||
- "Block EC2 instances without encryption" → [tfpolicy-author](references/tfpolicy-author.md)
|
||||
- "Convert this Sentinel policy to tfpolicy" → [tfpolicy-author](references/tfpolicy-author.md)
|
||||
- "Write a policytest for my EBS policy" → [tfpolicy-test](references/tfpolicy-test.md)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Wrong skill triggered?** Load the sub-skill directly from the routing table above.
|
||||
|
||||
```bash
|
||||
npx skills add hashicorp/agent-skills/terraform/terraform-policy/skills/tfpolicy-author
|
||||
npx skills add hashicorp/agent-skills/terraform/terraform-policy/skills/tfpolicy-test
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
name: terraform-policy-eval
|
||||
description: Auto-generated eval for terraform-policy.
|
||||
skill: terraform-policy
|
||||
version: "1.0"
|
||||
|
||||
config:
|
||||
trials_per_task: 1
|
||||
timeout_seconds: 300
|
||||
parallel: false
|
||||
executor: copilot-sdk
|
||||
model: claude-sonnet-4.6
|
||||
metrics:
|
||||
- name: task_completion
|
||||
weight: 0.7
|
||||
threshold: 0.8
|
||||
description: Did the skill complete trigger and anti-trigger checks?
|
||||
- name: efficiency
|
||||
weight: 0.3
|
||||
threshold: 0.7
|
||||
description: Did the skill stay within behavior limits?
|
||||
graders:
|
||||
- type: behavior
|
||||
name: token-budget
|
||||
config:
|
||||
max_tokens: 35000
|
||||
tasks:
|
||||
- "tasks/*.yaml"
|
||||
@@ -0,0 +1,17 @@
|
||||
id: negative-trigger-001
|
||||
name: Negative Trigger 1
|
||||
description: Auto-generated negative-trigger task.
|
||||
tags:
|
||||
- negative-trigger
|
||||
inputs:
|
||||
prompt: "Tell me a short joke about coffee."
|
||||
expected:
|
||||
should_trigger: false
|
||||
graders:
|
||||
- type: text
|
||||
name: omits-skill-keywords
|
||||
config:
|
||||
not_contains:
|
||||
- "policy"
|
||||
- "terraform"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
id: positive-trigger-001
|
||||
name: Positive Trigger 1
|
||||
description: Auto-generated positive-trigger task.
|
||||
tags:
|
||||
- positive-trigger
|
||||
inputs:
|
||||
prompt: "Use terraform-policy to help me complete this task"
|
||||
expected:
|
||||
should_trigger: true
|
||||
graders:
|
||||
- type: text
|
||||
name: contains-keywords
|
||||
config:
|
||||
contains:
|
||||
- "policy"
|
||||
- "terraform"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
id: positive-trigger-002
|
||||
name: Positive Trigger 2
|
||||
description: Auto-generated positive-trigger task.
|
||||
tags:
|
||||
- positive-trigger
|
||||
inputs:
|
||||
prompt: "I need assistance with terraform policy-related work"
|
||||
expected:
|
||||
should_trigger: true
|
||||
graders:
|
||||
- type: text
|
||||
name: contains-keywords
|
||||
config:
|
||||
contains:
|
||||
- "policy"
|
||||
- "terraform"
|
||||
|
||||
@@ -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.
|
||||
+19
@@ -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.
|
||||
+14
@@ -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"
|
||||
}
|
||||
}
|
||||
+61
@@ -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
|
||||
}
|
||||
+17
@@ -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.
|
||||
+13
@@ -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"
|
||||
}
|
||||
}
|
||||
+53
@@ -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
|
||||
}
|
||||
+17
@@ -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.
|
||||
+13
@@ -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"
|
||||
}
|
||||
}
|
||||
+55
@@ -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
|
||||
}
|
||||
+18
@@ -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.
|
||||
+14
@@ -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"
|
||||
}
|
||||
}
|
||||
+50
@@ -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
|
||||
}
|
||||
+20
@@ -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
|
||||
+24
@@ -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"
|
||||
}
|
||||
}
|
||||
+91
@@ -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
|
||||
}
|
||||
+22
@@ -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.
|
||||
+20
@@ -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"
|
||||
}
|
||||
}
|
||||
+94
@@ -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
|
||||
}
|
||||
+17
@@ -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.
|
||||
+9
@@ -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"
|
||||
}
|
||||
}
|
||||
+50
@@ -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
|
||||
}
|
||||
+17
@@ -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.
|
||||
+9
@@ -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"
|
||||
}
|
||||
}
|
||||
+52
@@ -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
|
||||
}
|
||||
+18
@@ -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.
|
||||
+14
@@ -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"
|
||||
}
|
||||
}
|
||||
+54
@@ -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
|
||||
}
|
||||
+18
@@ -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.
|
||||
+26
@@ -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'"
|
||||
}
|
||||
}
|
||||
+68
@@ -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
|
||||
}
|
||||
+18
@@ -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
|
||||
+14
@@ -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"
|
||||
}
|
||||
}
|
||||
+64
@@ -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
|
||||
}
|
||||
+20
@@ -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
|
||||
+22
@@ -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"
|
||||
}
|
||||
}
|
||||
+76
@@ -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
|
||||
}
|
||||
+25
@@ -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
|
||||
+27
@@ -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"
|
||||
}
|
||||
}
|
||||
+103
@@ -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
|
||||
}
|
||||
+18
@@ -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
|
||||
+23
@@ -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"
|
||||
}
|
||||
}
|
||||
+100
@@ -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
|
||||
}
|
||||
+18
@@ -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
|
||||
+22
@@ -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"
|
||||
}
|
||||
}
|
||||
+73
@@ -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
|
||||
}
|
||||
+17
@@ -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.
|
||||
+15
@@ -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"
|
||||
}
|
||||
}
|
||||
+56
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user