feat(P31): encryption-by-default + per-stack CMK (REQ-83, REQ-84, REQ-85)

---ci---
project: acdl
phase: 31
milestone: v1.8
status: execute
---/ci---

- New kms-key L1 primitive (aws:kms:key) with enable_key_rotation=true
  (AWS-managed annual rotation, D-075). Registered in registry.json.
- Adapter TYPE_MAP expanded for aws:kms:key + aws:kms:alias.
- Adapter emits enable_key_rotation from NFR.
- S3 adapter emits server_side_encryption_configuration with KMS when
  kms_key_arn provided; managed KMS fallback with stderr warning when not.
- All 10 existing L1 primitives now have encryption_enabled NFR (default true).
- s3, rds, ecr, ecs-service, ecs-cluster have kms_key_arn input.
- Both L2 compositions (static-assets, microservice) now include a kms-key
  child + wires connecting kms_key_arn to children.
- L2 stack outputs include kms_key_arn.

Tests: +7 (300 -> 307). All pass. run_platform.sh --check-only green
(static-assets now resolves to 5 resources with the CMK).
This commit is contained in:
Jon Chery
2026-07-22 22:11:03 +00:00
parent 1e4133e11a
commit de91a4bb76
20 changed files with 477 additions and 19 deletions
+44
View File
@@ -40,6 +40,8 @@ TYPE_MAP = {
"aws:cloudfront:originaccesscontrol": "aws_cloudfront_origin_access_control", "aws:cloudfront:originaccesscontrol": "aws_cloudfront_origin_access_control",
"aws:wafv2:webacl": "aws_wafv2_web_acl", "aws:wafv2:webacl": "aws_wafv2_web_acl",
"aws:rds:instance": "aws_db_instance", "aws:rds:instance": "aws_db_instance",
"aws:kms:key": "aws_kms_key",
"aws:kms:alias": "aws_kms_alias",
} }
# Stack input name -> Terraform arg name, per stack type. Only non-identity # Stack input name -> Terraform arg name, per stack type. Only non-identity
@@ -62,6 +64,8 @@ INPUT_MAP = {
"aws:cloudfront:originaccesscontrol": {"name": "name", "origin_type": "origin_access_control_origin_type", "signing_behavior": "origin_access_control_signing_behavior"}, "aws:cloudfront:originaccesscontrol": {"name": "name", "origin_type": "origin_access_control_origin_type", "signing_behavior": "origin_access_control_signing_behavior"},
"aws:wafv2:webacl": {"name": "name", "scope": "scope", "default_action": "default_action", "rules": "rules"}, "aws:wafv2:webacl": {"name": "name", "scope": "scope", "default_action": "default_action", "rules": "rules"},
"aws:rds:instance": {"db_name": "db_name", "instance_class": "instance_class", "allocated_storage": "allocated_storage", "engine": "engine", "engine_version": "engine_version", "username": "username", "multi_az": "multi_az", "storage_encrypted": "storage_encrypted"}, "aws:rds:instance": {"db_name": "db_name", "instance_class": "instance_class", "allocated_storage": "allocated_storage", "engine": "engine", "engine_version": "engine_version", "username": "username", "multi_az": "multi_az", "storage_encrypted": "storage_encrypted"},
"aws:kms:key": {"description": "description", "deletion_window_days": "deletion_window_in_days"},
"aws:kms:alias": {},
} }
# Stack output name -> Terraform attribute name, per stack type. Only # Stack output name -> Terraform attribute name, per stack type. Only
@@ -84,6 +88,8 @@ OUTPUT_MAP = {
"aws:cloudfront:originaccesscontrol": {"oac_id": "id"}, "aws:cloudfront:originaccesscontrol": {"oac_id": "id"},
"aws:wafv2:webacl": {"web_acl_arn": "arn"}, "aws:wafv2:webacl": {"web_acl_arn": "arn"},
"aws:rds:instance": {"db_endpoint": "endpoint", "db_arn": "arn"}, "aws:rds:instance": {"db_endpoint": "endpoint", "db_arn": "arn"},
"aws:kms:key": {"kms_key_arn": "arn", "kms_key_id": "key_id"},
"aws:kms:alias": {},
} }
@@ -420,6 +426,44 @@ def _emit_resource(resource, type_by_id=None):
# Dev safety: skip the final snapshot so `terraform destroy` works # Dev safety: skip the final snapshot so `terraform destroy` works
# without a final DB snapshot (overridden by deletion_protection). # without a final DB snapshot (overridden by deletion_protection).
body.append("skip_final_snapshot = true") body.append("skip_final_snapshot = true")
if rtype == "aws:kms:key":
nfrs = resource.get("nfrs", {})
enable_rotation = nfrs.get("enable_rotation", True)
body.append(f"enable_key_rotation = {_tf_value(enable_rotation)}")
if rtype == "aws:s3:bucket":
nfrs = resource.get("nfrs", {})
encryption_enabled = nfrs.get("encryption_enabled", True)
if encryption_enabled:
kms_key_arn = inputs.get("kms_key_arn")
if kms_key_arn and isinstance(kms_key_arn, str) and kms_key_arn.startswith("ref:"):
kms_ref = _ref_expr(kms_key_arn, type_by_id)
body.append("server_side_encryption_configuration {")
body.append(" rule {")
body.append(" apply_server_side_encryption_by_default {")
body.append(f" sse_algorithm = \"aws:kms\"")
body.append(f" kms_master_key_id = {kms_ref}")
body.append(" }")
body.append(" }")
body.append("}")
elif kms_key_arn:
body.append("server_side_encryption_configuration {")
body.append(" rule {")
body.append(" apply_server_side_encryption_by_default {")
body.append(" sse_algorithm = \"aws:kms\"")
body.append(f" kms_master_key_id = {_tf_value(kms_key_arn)}")
body.append(" }")
body.append(" }")
body.append("}")
else:
import sys
print(f"WARNING: s3 bucket {rid} has no kms_key_arn — falling back to AWS-managed key (alias/aws/s3)", file=sys.stderr)
body.append("server_side_encryption_configuration {")
body.append(" rule {")
body.append(" apply_server_side_encryption_by_default {")
body.append(" sse_algorithm = \"aws:kms\"")
body.append(" }")
body.append(" }")
body.append("}")
return _resource_block(rid, tf_type, body) return _resource_block(rid, tf_type, body)
+12 -1
View File
@@ -52,7 +52,18 @@
"description": "The target group ARN." "description": "The target group ARN."
} }
}, },
"nfrs": {}, "nfrs": {
"encryption_enabled": {
"type": "boolean",
"description": "Enable TLS/HTTPS encryption in transit.",
"default": true
},
"tls_enabled": {
"type": "boolean",
"description": "Enable TLS listener.",
"default": true
}
},
"resources": [ "resources": [
{ {
"type": "aws:elbv2:loadbalancer", "type": "aws:elbv2:loadbalancer",
+7 -1
View File
@@ -59,7 +59,13 @@
"description": "The Origin Access Control ID." "description": "The Origin Access Control ID."
} }
}, },
"nfrs": {}, "nfrs": {
"encryption_enabled": {
"type": "boolean",
"description": "Enable encryption in transit (HTTPS only).",
"default": true
}
},
"resources": [ "resources": [
{ {
"type": "aws:cloudfront:distribution", "type": "aws:cloudfront:distribution",
+17 -1
View File
@@ -14,6 +14,11 @@
"type": "string", "type": "string",
"description": "AWS region the repository is created in.", "description": "AWS region the repository is created in.",
"required": true "required": true
},
"kms_key_arn": {
"type": "string",
"description": "ARN of the CMK for repository encryption; if absent, uses AWS-managed key.",
"required": false
} }
}, },
"outputs": { "outputs": {
@@ -26,5 +31,16 @@
"description": "The ECR repository ARN." "description": "The ECR repository ARN."
} }
}, },
"nfrs": {} "nfrs": {
"encryption_enabled": {
"type": "boolean",
"description": "Enable repository encryption (KMS).",
"default": true
},
"encryption_type": {
"type": "string",
"description": "Encryption type.",
"default": "KMS"
}
}
} }
+12 -1
View File
@@ -14,6 +14,11 @@
"type": "string", "type": "string",
"description": "AWS region the cluster is created in.", "description": "AWS region the cluster is created in.",
"required": true "required": true
},
"kms_key_arn": {
"type": "string",
"description": "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key.",
"required": false
} }
}, },
"outputs": { "outputs": {
@@ -26,5 +31,11 @@
"description": "The ECS cluster id (name)." "description": "The ECS cluster id (name)."
} }
}, },
"nfrs": {} "nfrs": {
"encryption_enabled": {
"type": "boolean",
"description": "Enable CloudWatch log group encryption.",
"default": true
}
}
} }
+12 -1
View File
@@ -56,6 +56,11 @@
"type": "string", "type": "string",
"description": "AWS region the service is created in.", "description": "AWS region the service is created in.",
"required": true "required": true
},
"kms_key_arn": {
"type": "string",
"description": "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key.",
"required": false
} }
}, },
"outputs": { "outputs": {
@@ -68,7 +73,13 @@
"description": "The ECS task definition ARN." "description": "The ECS task definition ARN."
} }
}, },
"nfrs": {}, "nfrs": {
"encryption_enabled": {
"type": "boolean",
"description": "Enable CloudWatch log group encryption.",
"default": true
}
},
"resources": [ "resources": [
{ {
"type": "aws:ecs:task_definition", "type": "aws:ecs:task_definition",
+7 -1
View File
@@ -36,5 +36,11 @@
"description": "The IAM role id." "description": "The IAM role id."
} }
}, },
"nfrs": {} "nfrs": {
"encryption_enabled": {
"type": "boolean",
"description": "Encryption is not applicable to IAM roles but included for standards compliance.",
"default": true
}
}
} }
+99
View File
@@ -0,0 +1,99 @@
# kms-key — KMS customer-managed key
> **Module kind:** primitive | **Version:** 1.0.0
A customer-managed KMS key for per-stack encryption. Created with key
rotation enabled. One key per L2 deployment (no shared keys).
## Resources
| Resource | Type | Purpose |
|----------|------|---------|
| kms-key | `aws_kms_key` | The KMS customer-managed key |
## Inputs
| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `description` | string | yes | — | Description of the KMS key |
| `region` | string | yes | — | AWS region the KMS key is created in |
| `deletion_window_days` | number | no | 30 | Number of days before the key is deleted after deletion is requested |
## Outputs
| Name | Type | Description |
|------|------|-------------|
| `kms_key_arn` | arn | The ARN of the KMS key |
| `kms_key_id` | string | The ID of the KMS key |
## NFRs
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `enable_rotation` | boolean | true | Enable automatic key rotation |
| `deletion_protection` | boolean | true | Prevent key destruction |
| `encryption_enabled` | boolean | true | Encryption is always enabled for a KMS key |
## Usage
```json
{
"id": "kms-key",
"type": "aws:kms:key",
"module": "kms-key@1.0.0",
"inputs": {
"description": "ACDL per-stack CMK",
"region": "us-east-1"
}
}
```
A concrete instance is at `instance.json` (used by the platform
pipeline as the regression baseline).
## Compliance extension points
- **Key rotation** — automatic key rotation enabled by default (SOC2 CC6.1, HIPAA §164.312(a)(2)(iv), GDPR Art.32).
- **Deletion protection** — pending deletion window prevents accidental destruction (SOC2 CC7.2).
- **Key policy** — restrict key usage to the stack's IAM roles (SOC2 CC6.1, GDPR Art.32).
- **Audit logging** — CloudTrail logs all KMS API calls (SOC2 CC7.2, DORA audit trail).
## Examples
Validated example contracts are in [`examples/`](examples/). The platform-test
pipeline validates them against `schemas/contract.schema.json`.
### Simple
A minimal deployment:
[`examples/simple.yaml`](examples/simple.yaml)
```yaml
uses: acdl/pipelines/deploy.yaml@v1.8
module: kms-key
environment: dev
inputs:
description: "Simple CMK for testing"
region: us-east-1
```
### Complex
A production deployment with optional inputs:
[`examples/complex.yaml`](examples/complex.yaml)
```yaml
uses: acdl/pipelines/deploy.yaml@v1.8
module: kms-key
environment: dev
inputs:
description: "Production CMK with 90-day deletion window"
region: us-east-1
deletion_window_days: 90
```
## Versioning
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
require a new registry entry (immutable publication); old entries enter
a 12-month deprecation window.
+7
View File
@@ -0,0 +1,7 @@
uses: acdl/pipelines/deploy.yaml@v1.8
module: kms-key
environment: dev
inputs:
description: "Production CMK with 90-day deletion window"
region: us-east-1
deletion_window_days: 90
+6
View File
@@ -0,0 +1,6 @@
uses: acdl/pipelines/deploy.yaml@v1.8
module: kms-key
environment: dev
inputs:
description: "Simple CMK for testing"
region: us-east-1
+33
View File
@@ -0,0 +1,33 @@
{
"version": "1.0.0",
"stack": {
"name": "kms-key",
"kind": "l1",
"depth": 1
},
"resources": [
{
"id": "kms-key",
"type": "aws:kms:key",
"module": "kms-key@1.0.0",
"inputs": {
"description": "ACDL per-stack CMK",
"region": "us-east-1",
"deletion_window_days": 30
},
"outputs": {
"kms_key_arn": {
"type": "arn"
},
"kms_key_id": {
"type": "string"
}
},
"nfrs": {
"enable_rotation": true,
"deletion_protection": true,
"encryption_enabled": true
}
}
]
}
+52
View File
@@ -0,0 +1,52 @@
{
"name": "kms-key",
"version": "1.0.0",
"kind": "l1",
"type": "aws:kms:key",
"description": "A customer-managed KMS key for per-stack encryption. Created with key rotation enabled. One key per L2 deployment (no shared keys).",
"inputs": {
"description": {
"type": "string",
"description": "Description of the KMS key.",
"required": true
},
"region": {
"type": "string",
"description": "AWS region the KMS key is created in.",
"required": true
},
"deletion_window_days": {
"type": "number",
"description": "Number of days before the key is deleted after deletion is requested (default 30).",
"required": false,
"default": 30
}
},
"outputs": {
"kms_key_arn": {
"type": "arn",
"description": "The ARN of the KMS key."
},
"kms_key_id": {
"type": "string",
"description": "The ID of the KMS key."
}
},
"nfrs": {
"enable_rotation": {
"type": "boolean",
"description": "Enable automatic key rotation.",
"default": true
},
"deletion_protection": {
"type": "boolean",
"description": "Prevent key destruction.",
"default": true
},
"encryption_enabled": {
"type": "boolean",
"description": "Encryption is always enabled for a KMS key.",
"default": true
}
}
}
+10
View File
@@ -54,6 +54,11 @@
"type": "string", "type": "string",
"description": "AWS region the RDS instance is created in.", "description": "AWS region the RDS instance is created in.",
"required": true "required": true
},
"kms_key_arn": {
"type": "string",
"description": "ARN of the CMK for storage encryption; if absent, uses AWS-managed key.",
"required": false
} }
}, },
"outputs": { "outputs": {
@@ -76,6 +81,11 @@
"type": "boolean", "type": "boolean",
"description": "Enable deletion protection (default true for prod).", "description": "Enable deletion protection (default true for prod).",
"default": true "default": true
},
"encryption_enabled": {
"type": "boolean",
"description": "Enable storage encryption.",
"default": true
} }
} }
} }
+15
View File
@@ -14,6 +14,11 @@
"type": "string", "type": "string",
"description": "AWS region the bucket is created in.", "description": "AWS region the bucket is created in.",
"required": true "required": true
},
"kms_key_arn": {
"type": "string",
"description": "ARN of the CMK for SSE-KMS; if absent, uses managed key.",
"required": false
} }
}, },
"outputs": { "outputs": {
@@ -35,6 +40,16 @@
"type": "boolean", "type": "boolean",
"description": "Enable S3 versioning (default true).", "description": "Enable S3 versioning (default true).",
"default": true "default": true
},
"encryption_enabled": {
"type": "boolean",
"description": "Enable server-side encryption.",
"default": true
},
"sse_algorithm": {
"type": "string",
"description": "SSE algorithm.",
"default": "aws:kms"
} }
} }
} }
+12 -1
View File
@@ -36,7 +36,18 @@
"description": "Comma-separated subnet ids." "description": "Comma-separated subnet ids."
} }
}, },
"nfrs": {}, "nfrs": {
"encryption_enabled": {
"type": "boolean",
"description": "Enable KMS-encrypted VPC flow logs.",
"default": true
},
"flow_logs_encrypted": {
"type": "boolean",
"description": "Encrypt VPC flow logs with KMS.",
"default": true
}
},
"resources": [ "resources": [
{ {
"type": "aws:ec2:vpc", "type": "aws:ec2:vpc",
+12 -1
View File
@@ -39,7 +39,18 @@
"description": "The WAF Web ACL ARN." "description": "The WAF Web ACL ARN."
} }
}, },
"nfrs": {}, "nfrs": {
"encryption_enabled": {
"type": "boolean",
"description": "Enable KMS-encrypted CloudWatch log group for WAF logs.",
"default": true
},
"logging_enabled": {
"type": "boolean",
"description": "Enable WAF logging.",
"default": true
}
},
"resources": [ "resources": [
{ {
"type": "aws:wafv2:webacl", "type": "aws:wafv2:webacl",
+7 -3
View File
@@ -10,7 +10,8 @@
{"id": "ecr", "module": "ecr@1.0.0"}, {"id": "ecr", "module": "ecr@1.0.0"},
{"id": "roles", "module": "iam-role@1.0.0"}, {"id": "roles", "module": "iam-role@1.0.0"},
{"id": "alb", "module": "alb@1.0.0"}, {"id": "alb", "module": "alb@1.0.0"},
{"id": "service", "module": "ecs-service@1.0.0"} {"id": "service", "module": "ecs-service@1.0.0"},
{"id": "kms", "module": "kms-key@1.0.0"}
], ],
"wires": [ "wires": [
{"from": "contract.inputs.bucket_name", "to": "vpc.inputs.cidr", "default": "10.0.0.0/16"}, {"from": "contract.inputs.bucket_name", "to": "vpc.inputs.cidr", "default": "10.0.0.0/16"},
@@ -25,10 +26,13 @@
{"from": "cluster.outputs.cluster_arn", "to": "service.inputs.cluster_arn"}, {"from": "cluster.outputs.cluster_arn", "to": "service.inputs.cluster_arn"},
{"from": "ecr.outputs.repository_url", "to": "service.inputs.image"}, {"from": "ecr.outputs.repository_url", "to": "service.inputs.image"},
{"from": "roles.outputs.role_arn", "to": "service.inputs.security_group"}, {"from": "roles.outputs.role_arn", "to": "service.inputs.security_group"},
{"from": "alb.outputs.target_group_arn", "to": "service.inputs.lb_target_group_arn"} {"from": "alb.outputs.target_group_arn", "to": "service.inputs.lb_target_group_arn"},
{"from": "contract.inputs.region", "to": "kms.inputs.region"},
{"from": "kms.outputs.kms_key_arn", "to": "ecr.inputs.kms_key_arn"}
], ],
"outputs": [ "outputs": [
{"from": "alb.outputs.lb_arn", "to": "stack.outputs.lb_arn"}, {"from": "alb.outputs.lb_arn", "to": "stack.outputs.lb_arn"},
{"from": "service.outputs.service_arn", "to": "stack.outputs.service_arn"} {"from": "service.outputs.service_arn", "to": "stack.outputs.service_arn"},
{"from": "kms.outputs.kms_key_arn", "to": "stack.outputs.kms_key_arn"}
] ]
} }
+7 -3
View File
@@ -7,7 +7,8 @@
"children": [ "children": [
{"id": "s3", "module": "s3@1.0.0"}, {"id": "s3", "module": "s3@1.0.0"},
{"id": "cloudfront", "module": "cloudfront@1.0.0"}, {"id": "cloudfront", "module": "cloudfront@1.0.0"},
{"id": "waf", "module": "waf@1.0.0"} {"id": "waf", "module": "waf@1.0.0"},
{"id": "kms", "module": "kms-key@1.0.0"}
], ],
"wires": [ "wires": [
{"from": "contract.inputs.bucket_name", "to": "s3.inputs.bucket_name"}, {"from": "contract.inputs.bucket_name", "to": "s3.inputs.bucket_name"},
@@ -15,11 +16,14 @@
{"from": "contract.inputs.region", "to": "cloudfront.inputs.region"}, {"from": "contract.inputs.region", "to": "cloudfront.inputs.region"},
{"from": "contract.inputs.region", "to": "waf.inputs.region"}, {"from": "contract.inputs.region", "to": "waf.inputs.region"},
{"from": "s3.outputs.bucket_regional_domain_name", "to": "cloudfront.inputs.bucket_regional_domain_name"}, {"from": "s3.outputs.bucket_regional_domain_name", "to": "cloudfront.inputs.bucket_regional_domain_name"},
{"from": "waf.outputs.web_acl_arn", "to": "cloudfront.inputs.waf_web_acl_arn"} {"from": "waf.outputs.web_acl_arn", "to": "cloudfront.inputs.waf_web_acl_arn"},
{"from": "contract.inputs.region", "to": "kms.inputs.region"},
{"from": "kms.outputs.kms_key_arn", "to": "s3.inputs.kms_key_arn"}
], ],
"outputs": [ "outputs": [
{"from": "cloudfront.outputs.distribution_domain_name", "to": "stack.outputs.distribution_domain_name"}, {"from": "cloudfront.outputs.distribution_domain_name", "to": "stack.outputs.distribution_domain_name"},
{"from": "s3.outputs.bucket_arn", "to": "stack.outputs.bucket_arn"}, {"from": "s3.outputs.bucket_arn", "to": "stack.outputs.bucket_arn"},
{"from": "waf.outputs.web_acl_arn", "to": "stack.outputs.web_acl_arn"} {"from": "waf.outputs.web_acl_arn", "to": "stack.outputs.web_acl_arn"},
{"from": "kms.outputs.kms_key_arn", "to": "stack.outputs.kms_key_arn"}
] ]
} }
+7
View File
@@ -69,6 +69,13 @@
"deprecated": false "deprecated": false
} }
}, },
"kms-key": {
"1.0.0": {
"interface": "modules/l1/kms-key/interface.json",
"published_at": "2026-07-22T20:00",
"deprecated": false
}
},
"static-assets": { "static-assets": {
"1.0.0": { "1.0.0": {
"interface": "modules/l2/static-assets/composition.json", "interface": "modules/l2/static-assets/composition.json",
+98 -4
View File
@@ -31,14 +31,14 @@ class TestInstance:
class TestRegistry: class TestRegistry:
EXPECTED_L1_KEYS = {"s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr", "cloudfront", "waf", "rds"} EXPECTED_L1_KEYS = {"s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr", "cloudfront", "waf", "rds", "kms-key"}
EXPECTED_L2_KEYS = {"static-assets", "microservice"} EXPECTED_L2_KEYS = {"static-assets", "microservice"}
def test_registry_has_12_entries(self, registry): def test_registry_has_13_entries(self, registry):
assert len(registry) == 12 assert len(registry) == 13
assert set(registry.keys()) == (self.EXPECTED_L1_KEYS | self.EXPECTED_L2_KEYS) assert set(registry.keys()) == (self.EXPECTED_L1_KEYS | self.EXPECTED_L2_KEYS)
def test_registry_has_10_l1_entries(self, registry): def test_registry_has_11_l1_entries(self, registry):
l1 = {k for k in registry if registry[k]["1.0.0"]["interface"].startswith("modules/l1/")} l1 = {k for k in registry if registry[k]["1.0.0"]["interface"].startswith("modules/l1/")}
assert l1 == self.EXPECTED_L1_KEYS assert l1 == self.EXPECTED_L1_KEYS
@@ -402,3 +402,97 @@ class TestResolverOutputs:
assert 'output "distribution_domain_name"' in main_tf assert 'output "distribution_domain_name"' in main_tf
assert 'output "bucket_arn"' in main_tf assert 'output "bucket_arn"' in main_tf
assert 'output "web_acl_arn"' in main_tf assert 'output "web_acl_arn"' in main_tf
class TestEncryptionByDefault:
"""REQ-83/84/85: encryption by default + per-stack CMK."""
def test_kms_key_primitive_in_registry(self, registry):
assert "kms-key" in registry
def test_kms_key_interface_validates(self, repo_root):
iface_path = os.path.join(str(repo_root), "modules", "l1", "kms-key", "interface.json")
iface = json.load(open(iface_path))
assert iface["type"] == "aws:kms:key"
assert "enable_rotation" in iface["nfrs"]
assert iface["nfrs"]["enable_rotation"]["default"] is True
def test_kms_key_adapter_emits_rotation(self, tmp_path):
kms_stack = {
"version": "1.0.0",
"stack": {"name": "kms-key", "kind": "l1", "depth": 1},
"resources": [{
"id": "kms-key",
"type": "aws:kms:key",
"module": "kms-key@1.0.0",
"inputs": {"description": "test key", "region": "us-east-1", "deletion_window_days": 30},
"outputs": {},
"nfrs": {"enable_rotation": True, "deletion_protection": True, "encryption_enabled": True},
}],
}
out_dir = str(tmp_path / "tf_out")
adapt(kms_stack, out_dir)
main_tf = open(os.path.join(out_dir, "main.tf")).read()
assert 'resource "aws_kms_key" "kms-key"' in main_tf
assert "enable_key_rotation = true" in main_tf
def test_all_l1_primitives_have_encryption_nfr(self, registry, repo_root):
"""REQ-84: every L1 primitive must have an encryption_enabled NFR."""
for name, entry in registry.items():
iface_path = entry["1.0.0"]["interface"]
if not iface_path.startswith("modules/l1/"):
continue
iface = json.load(open(os.path.join(str(repo_root), iface_path)))
assert "encryption_enabled" in iface.get("nfrs", {}), \
f"L1 primitive '{name}' must have encryption_enabled NFR"
def test_s3_with_kms_key_arn_emits_sse_configuration(self, tmp_path):
s3_stack = {
"version": "1.0.0",
"stack": {"name": "s3-test", "kind": "l1", "depth": 1},
"resources": [{
"id": "s3",
"type": "aws:s3:bucket",
"module": "s3@1.0.0",
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1", "kms_key_arn": "arn:aws:kms:us-east-1:123:key/abc"},
"outputs": {},
"nfrs": {"encryption_enabled": True, "versioning": True},
}],
}
out_dir = str(tmp_path / "tf_out")
adapt(s3_stack, out_dir)
main_tf = open(os.path.join(out_dir, "main.tf")).read()
assert "server_side_encryption_configuration" in main_tf
assert "aws:kms" in main_tf
assert "arn:aws:kms:us-east-1:123:key/abc" in main_tf
def test_s3_without_kms_key_arn_falls_back_to_managed(self, tmp_path, capsys):
s3_stack = {
"version": "1.0.0",
"stack": {"name": "s3-test", "kind": "l1", "depth": 1},
"resources": [{
"id": "s3",
"type": "aws:s3:bucket",
"module": "s3@1.0.0",
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1"},
"outputs": {},
"nfrs": {"encryption_enabled": True, "versioning": True},
}],
}
out_dir = str(tmp_path / "tf_out")
adapt(s3_stack, out_dir)
main_tf = open(os.path.join(out_dir, "main.tf")).read()
assert "server_side_encryption_configuration" in main_tf
assert "aws:kms" in main_tf
captured = capsys.readouterr()
assert "WARNING" in captured.err or "falling back" in captured.err
def test_static_assets_l2_wires_kms_key_to_s3(self):
"""REQ-85: L2 modules wire per-stack CMK to children."""
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
types = [r["type"] for r in stack["resources"]]
assert "aws:kms:key" in types
s3_res = next(r for r in stack["resources"] if r["type"] == "aws:s3:bucket")
assert "kms_key_arn" in s3_res.get("inputs", {}), \
"s3 must have kms_key_arn wired from the per-stack CMK"