mirror of
https://github.com/Sea-Haven-Industries/apm-wo-analysis.git
synced 2026-09-30 06:33:14 +00:00
178 lines
5.7 KiB
Python
178 lines
5.7 KiB
Python
|
|
"""Synth-level assertions for the Grafana stack (Phase 5).
|
||
|
|
|
||
|
|
Synthesizes ``apm-wo-analysis-grafana`` and asserts the security posture that
|
||
|
|
can't be eyeballed: the ALB only admits the office CIDRs on 443 (never
|
||
|
|
0.0.0.0/0), the instance only takes traffic from the ALB SG, the instance role
|
||
|
|
carries no static keys and only scoped Athena/Glue-read/S3 access, the root
|
||
|
|
volume is gp3 + retained, a daily DLM backup exists, and grafana.seahaven.com
|
||
|
|
aliases the ALB. No AWS, no Docker (bundling skipped).
|
||
|
|
|
||
|
|
Run with the repo venv:
|
||
|
|
|
||
|
|
python -m pytest tests/test_grafana_synth.py -q
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import aws_cdk as cdk
|
||
|
|
from aws_cdk.assertions import Match, Template
|
||
|
|
|
||
|
|
CDK_DIR = Path(__file__).resolve().parents[1] / "cdk"
|
||
|
|
sys.path.insert(0, str(CDK_DIR))
|
||
|
|
|
||
|
|
from stacks.grafana_stack import GrafanaStack # noqa: E402
|
||
|
|
|
||
|
|
OFFICE_CIDRS = {"47.21.61.4/32", "96.250.164.146/32"}
|
||
|
|
|
||
|
|
|
||
|
|
def _cdk_context() -> dict:
|
||
|
|
ctx = json.loads((CDK_DIR / "cdk.json").read_text())["context"]
|
||
|
|
ctx["aws:cdk:bundling-stacks"] = []
|
||
|
|
return ctx
|
||
|
|
|
||
|
|
|
||
|
|
def _template() -> Template:
|
||
|
|
app = cdk.App(context=_cdk_context())
|
||
|
|
stack = GrafanaStack(
|
||
|
|
app,
|
||
|
|
"apm-wo-analysis-grafana",
|
||
|
|
env=cdk.Environment(account="328440206208", region="us-east-1"),
|
||
|
|
)
|
||
|
|
return Template.from_stack(stack)
|
||
|
|
|
||
|
|
|
||
|
|
def _all_cidr_ingress(t: Template):
|
||
|
|
"""Every CIDR-based ingress rule, inline on SGs and standalone, as
|
||
|
|
(cidr, from_port, to_port) tuples."""
|
||
|
|
rules = []
|
||
|
|
for sg in t.find_resources("AWS::EC2::SecurityGroup").values():
|
||
|
|
for r in sg["Properties"].get("SecurityGroupIngress", []):
|
||
|
|
if "CidrIp" in r:
|
||
|
|
rules.append((r["CidrIp"], r.get("FromPort"), r.get("ToPort")))
|
||
|
|
for ing in t.find_resources("AWS::EC2::SecurityGroupIngress").values():
|
||
|
|
p = ing["Properties"]
|
||
|
|
if "CidrIp" in p:
|
||
|
|
rules.append((p["CidrIp"], p.get("FromPort"), p.get("ToPort")))
|
||
|
|
return rules
|
||
|
|
|
||
|
|
|
||
|
|
def test_alb_only_admits_office_cidrs_on_443():
|
||
|
|
rules = _all_cidr_ingress(_template())
|
||
|
|
cidrs_443 = {c for c, fp, tp in rules if fp == 443 and tp == 443}
|
||
|
|
assert cidrs_443 == OFFICE_CIDRS, (
|
||
|
|
f"443 ingress should be office-only, got {cidrs_443}"
|
||
|
|
)
|
||
|
|
# Nothing anywhere may be open to the world.
|
||
|
|
assert all(c != "0.0.0.0/0" for c, _, _ in rules), "found a 0.0.0.0/0 ingress"
|
||
|
|
|
||
|
|
|
||
|
|
def test_instance_only_reachable_from_alb_on_3000():
|
||
|
|
# The instance SG ingress on 3000 is a SourceSecurityGroup rule, not a CIDR.
|
||
|
|
_template().has_resource_properties(
|
||
|
|
"AWS::EC2::SecurityGroupIngress",
|
||
|
|
Match.object_like(
|
||
|
|
{
|
||
|
|
"FromPort": 3000,
|
||
|
|
"ToPort": 3000,
|
||
|
|
"SourceSecurityGroupId": Match.any_value(),
|
||
|
|
}
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_alb_internet_facing_https_listener():
|
||
|
|
t = _template()
|
||
|
|
t.has_resource_properties(
|
||
|
|
"AWS::ElasticLoadBalancingV2::LoadBalancer", {"Scheme": "internet-facing"}
|
||
|
|
)
|
||
|
|
t.has_resource_properties(
|
||
|
|
"AWS::ElasticLoadBalancingV2::Listener",
|
||
|
|
Match.object_like(
|
||
|
|
{"Port": 443, "Protocol": "HTTPS", "Certificates": Match.any_value()}
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_no_static_keys_in_stack():
|
||
|
|
t = _template()
|
||
|
|
t.resource_count_is("AWS::IAM::User", 0)
|
||
|
|
t.resource_count_is("AWS::IAM::AccessKey", 0)
|
||
|
|
|
||
|
|
|
||
|
|
def test_instance_role_scoped_and_uses_ssm():
|
||
|
|
t = _template()
|
||
|
|
# Session Manager (no SSH) — the SSM managed policy is attached.
|
||
|
|
t.has_resource_properties(
|
||
|
|
"AWS::IAM::Role",
|
||
|
|
Match.object_like(
|
||
|
|
{
|
||
|
|
"ManagedPolicyArns": Match.array_with(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"Fn::Join": [
|
||
|
|
"",
|
||
|
|
Match.array_with(
|
||
|
|
[":iam::aws:policy/AmazonSSMManagedInstanceCore"]
|
||
|
|
),
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
)
|
||
|
|
}
|
||
|
|
),
|
||
|
|
)
|
||
|
|
# The instance role must not be able to write the catalog or run wide Athena.
|
||
|
|
for policy in t.find_resources("AWS::IAM::Policy").values():
|
||
|
|
for stmt in policy["Properties"]["PolicyDocument"]["Statement"]:
|
||
|
|
actions = stmt.get("Action", [])
|
||
|
|
actions = [actions] if isinstance(actions, str) else actions
|
||
|
|
for a in actions:
|
||
|
|
if isinstance(a, str):
|
||
|
|
assert a not in ("glue:*", "athena:*", "s3:*", "*"), (
|
||
|
|
f"too broad: {a}"
|
||
|
|
)
|
||
|
|
assert not a.startswith("glue:Create"), f"no Glue writes: {a}"
|
||
|
|
assert not a.startswith("glue:Update"), f"no Glue writes: {a}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_root_volume_gp3_and_retained():
|
||
|
|
_template().has_resource_properties(
|
||
|
|
"AWS::EC2::Instance",
|
||
|
|
Match.object_like(
|
||
|
|
{
|
||
|
|
"BlockDeviceMappings": Match.array_with(
|
||
|
|
[
|
||
|
|
Match.object_like(
|
||
|
|
{
|
||
|
|
"Ebs": Match.object_like(
|
||
|
|
{"VolumeType": "gp3", "DeleteOnTermination": False}
|
||
|
|
)
|
||
|
|
}
|
||
|
|
)
|
||
|
|
]
|
||
|
|
)
|
||
|
|
}
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_daily_dlm_backup_enabled():
|
||
|
|
_template().has_resource_properties(
|
||
|
|
"AWS::DLM::LifecyclePolicy",
|
||
|
|
Match.object_like(
|
||
|
|
{
|
||
|
|
"State": "ENABLED",
|
||
|
|
"PolicyDetails": Match.object_like({"ResourceTypes": ["INSTANCE"]}),
|
||
|
|
}
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_route53_alias_for_grafana():
|
||
|
|
_template().has_resource_properties(
|
||
|
|
"AWS::Route53::RecordSet",
|
||
|
|
Match.object_like({"Type": "A", "Name": "grafana.seahaven.com."}),
|
||
|
|
)
|