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:
+99
-5
@@ -31,14 +31,14 @@ class TestInstance:
|
||||
|
||||
|
||||
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"}
|
||||
|
||||
def test_registry_has_12_entries(self, registry):
|
||||
assert len(registry) == 12
|
||||
def test_registry_has_13_entries(self, registry):
|
||||
assert len(registry) == 13
|
||||
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/")}
|
||||
assert l1 == self.EXPECTED_L1_KEYS
|
||||
|
||||
@@ -401,4 +401,98 @@ class TestResolverOutputs:
|
||||
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||
assert 'output "distribution_domain_name"' 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"
|
||||
Reference in New Issue
Block a user