mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-10 21:24:56 +03:00
511 lines
21 KiB
Python
511 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
# For authorized cloud security assessments only
|
|
"""AWS Nitro Enclave Security Agent - Validates enclave attestation, audits KMS policies, and verifies enclave isolation."""
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import socket
|
|
import struct
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
try:
|
|
import boto3
|
|
from botocore.exceptions import ClientError
|
|
except ImportError:
|
|
print("ERROR: boto3 required. Install with: pip install boto3")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
import cbor2
|
|
except ImportError:
|
|
cbor2 = None
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_nitro_instances(ec2_client, region):
|
|
"""Find EC2 instances with Nitro Enclave support enabled."""
|
|
findings = []
|
|
paginator = ec2_client.get_paginator("describe_instances")
|
|
for page in paginator.paginate(
|
|
Filters=[{"Name": "enclave-options.enabled", "Values": ["true"]}]
|
|
):
|
|
for reservation in page["Reservations"]:
|
|
for instance in reservation["Instances"]:
|
|
instance_info = {
|
|
"instance_id": instance["InstanceId"],
|
|
"instance_type": instance["InstanceType"],
|
|
"state": instance["State"]["Name"],
|
|
"enclave_enabled": True,
|
|
"iam_role": None,
|
|
"launch_time": instance.get("LaunchTime", "").isoformat() if instance.get("LaunchTime") else None,
|
|
"region": region,
|
|
}
|
|
if instance.get("IamInstanceProfile"):
|
|
instance_info["iam_role"] = instance["IamInstanceProfile"]["Arn"]
|
|
findings.append(instance_info)
|
|
logger.info("Found %d Nitro Enclave-enabled instances in %s", len(findings), region)
|
|
return findings
|
|
|
|
|
|
def audit_kms_key_policy(kms_client, key_id):
|
|
"""Audit a KMS key policy for Nitro Enclave attestation conditions."""
|
|
result = {
|
|
"key_id": key_id,
|
|
"has_attestation_condition": False,
|
|
"pcr_conditions": [],
|
|
"image_sha_condition": False,
|
|
"allowed_principals": [],
|
|
"allowed_actions": [],
|
|
"issues": [],
|
|
}
|
|
try:
|
|
key_meta = kms_client.describe_key(KeyId=key_id)
|
|
result["key_arn"] = key_meta["KeyMetadata"]["Arn"]
|
|
result["key_state"] = key_meta["KeyMetadata"]["KeyState"]
|
|
result["key_usage"] = key_meta["KeyMetadata"]["KeyUsage"]
|
|
|
|
policy_json = kms_client.get_key_policy(KeyId=key_id, PolicyName="default")["Policy"]
|
|
policy = json.loads(policy_json)
|
|
|
|
for statement in policy.get("Statement", []):
|
|
principals = statement.get("Principal", {})
|
|
actions = statement.get("Action", [])
|
|
if isinstance(actions, str):
|
|
actions = [actions]
|
|
conditions = statement.get("Condition", {})
|
|
|
|
for action in actions:
|
|
if action not in result["allowed_actions"]:
|
|
result["allowed_actions"].append(action)
|
|
|
|
if isinstance(principals, dict) and "AWS" in principals:
|
|
aws_principals = principals["AWS"]
|
|
if isinstance(aws_principals, str):
|
|
aws_principals = [aws_principals]
|
|
result["allowed_principals"].extend(aws_principals)
|
|
|
|
# Check for attestation conditions
|
|
for operator_key, operator_conditions in conditions.items():
|
|
for cond_key, cond_value in operator_conditions.items():
|
|
if "RecipientAttestation" in cond_key:
|
|
result["has_attestation_condition"] = True
|
|
if "ImageSha384" in cond_key:
|
|
result["image_sha_condition"] = True
|
|
result["pcr_conditions"].append({
|
|
"type": "ImageSha384 (PCR0)",
|
|
"operator": operator_key,
|
|
"value": cond_value[:32] + "..." if len(str(cond_value)) > 32 else cond_value,
|
|
})
|
|
elif "PCR" in cond_key:
|
|
pcr_id = cond_key.split(":")[-1]
|
|
result["pcr_conditions"].append({
|
|
"type": pcr_id,
|
|
"operator": operator_key,
|
|
"value": cond_value[:32] + "..." if len(str(cond_value)) > 32 else cond_value,
|
|
})
|
|
|
|
# Check for missing attestation on decrypt actions
|
|
has_decrypt = any("Decrypt" in a or "GenerateDataKey" in a for a in actions)
|
|
if has_decrypt and not any("RecipientAttestation" in str(conditions)):
|
|
if statement.get("Effect") == "Allow":
|
|
result["issues"].append(
|
|
f"Statement '{statement.get('Sid', 'unnamed')}' allows Decrypt/GenerateDataKey "
|
|
f"without kms:RecipientAttestation condition - parent instance can decrypt directly"
|
|
)
|
|
|
|
if not result["has_attestation_condition"]:
|
|
result["issues"].append(
|
|
"KMS key policy has no RecipientAttestation conditions - "
|
|
"decryption is not restricted to verified enclaves"
|
|
)
|
|
|
|
except ClientError as e:
|
|
result["issues"].append(f"Error accessing key: {e.response['Error']['Message']}")
|
|
|
|
return result
|
|
|
|
|
|
def audit_iam_role_for_enclave(iam_client, role_name):
|
|
"""Check if an IAM role has appropriate permissions for enclave operations."""
|
|
result = {
|
|
"role_name": role_name,
|
|
"has_kms_permissions": False,
|
|
"kms_actions": [],
|
|
"has_ec2_enclave_permissions": False,
|
|
"overprivileged": False,
|
|
"issues": [],
|
|
}
|
|
try:
|
|
# Check attached policies
|
|
attached = iam_client.list_attached_role_policies(RoleName=role_name)
|
|
for policy in attached["AttachedPolicies"]:
|
|
if policy["PolicyName"] == "AdministratorAccess":
|
|
result["overprivileged"] = True
|
|
result["issues"].append(
|
|
"Role has AdministratorAccess - violates least privilege for enclave workloads"
|
|
)
|
|
|
|
policy_version = iam_client.get_policy(PolicyArn=policy["PolicyArn"])
|
|
version_id = policy_version["Policy"]["DefaultVersionId"]
|
|
policy_doc = iam_client.get_policy_version(
|
|
PolicyArn=policy["PolicyArn"], VersionId=version_id
|
|
)
|
|
for stmt in policy_doc["PolicyVersion"]["Document"].get("Statement", []):
|
|
actions = stmt.get("Action", [])
|
|
if isinstance(actions, str):
|
|
actions = [actions]
|
|
for action in actions:
|
|
if "kms:" in action:
|
|
result["has_kms_permissions"] = True
|
|
result["kms_actions"].append(action)
|
|
if action in ("kms:*", "*"):
|
|
result["overprivileged"] = True
|
|
result["issues"].append(
|
|
f"Role has wildcard KMS permissions ({action}) - should restrict to specific keys"
|
|
)
|
|
|
|
# Check inline policies
|
|
inline = iam_client.list_role_policies(RoleName=role_name)
|
|
for policy_name in inline["PolicyNames"]:
|
|
policy_doc = iam_client.get_role_policy(RoleName=role_name, PolicyName=policy_name)
|
|
for stmt in policy_doc["PolicyDocument"].get("Statement", []):
|
|
actions = stmt.get("Action", [])
|
|
if isinstance(actions, str):
|
|
actions = [actions]
|
|
resources = stmt.get("Resource", [])
|
|
if isinstance(resources, str):
|
|
resources = [resources]
|
|
for action in actions:
|
|
if "kms:" in action:
|
|
result["has_kms_permissions"] = True
|
|
result["kms_actions"].append(action)
|
|
if "*" in resources:
|
|
result["issues"].append(
|
|
f"Inline policy '{policy_name}' uses wildcard Resource - restrict to specific KMS key ARNs"
|
|
)
|
|
|
|
if not result["has_kms_permissions"]:
|
|
result["issues"].append("Role has no KMS permissions - cannot perform enclave-side decryption")
|
|
|
|
except ClientError as e:
|
|
result["issues"].append(f"Error auditing role: {e.response['Error']['Message']}")
|
|
|
|
return result
|
|
|
|
|
|
def check_enclave_allocator_config(instance_id, ssm_client):
|
|
"""Check enclave allocator configuration via SSM (if available)."""
|
|
result = {
|
|
"instance_id": instance_id,
|
|
"allocator_configured": False,
|
|
"memory_mib": None,
|
|
"cpu_count": None,
|
|
"issues": [],
|
|
}
|
|
try:
|
|
response = ssm_client.send_command(
|
|
InstanceIds=[instance_id],
|
|
DocumentName="AWS-RunShellScript",
|
|
Parameters={
|
|
"commands": ["cat /etc/nitro_enclaves/allocator.yaml 2>/dev/null || echo 'NOT_FOUND'"]
|
|
},
|
|
)
|
|
command_id = response["Command"]["CommandId"]
|
|
|
|
import time
|
|
time.sleep(3)
|
|
|
|
output = ssm_client.get_command_invocation(
|
|
CommandId=command_id, InstanceId=instance_id
|
|
)
|
|
stdout = output.get("StandardOutputContent", "")
|
|
|
|
if "NOT_FOUND" in stdout:
|
|
result["issues"].append("Allocator config not found at /etc/nitro_enclaves/allocator.yaml")
|
|
else:
|
|
result["allocator_configured"] = True
|
|
for line in stdout.splitlines():
|
|
line = line.strip()
|
|
if line.startswith("memory_mib:"):
|
|
result["memory_mib"] = int(line.split(":")[1].strip())
|
|
elif line.startswith("cpu_count:"):
|
|
result["cpu_count"] = int(line.split(":")[1].strip())
|
|
|
|
if result["memory_mib"] and result["memory_mib"] < 512:
|
|
result["issues"].append(
|
|
f"Allocated memory ({result['memory_mib']} MiB) is very low - may cause enclave launch failures"
|
|
)
|
|
if result["cpu_count"] and result["cpu_count"] < 2:
|
|
result["issues"].append(
|
|
f"Allocated CPUs ({result['cpu_count']}) is minimal - consider 2+ for production"
|
|
)
|
|
|
|
except ClientError as e:
|
|
result["issues"].append(f"SSM access failed: {e.response['Error']['Message']}")
|
|
|
|
return result
|
|
|
|
|
|
def validate_attestation_document_structure(attestation_b64):
|
|
"""Validate the structure of a base64-encoded attestation document."""
|
|
if cbor2 is None:
|
|
return {"error": "cbor2 package required for attestation validation. Install with: pip install cbor2"}
|
|
|
|
result = {
|
|
"valid_structure": False,
|
|
"pcrs": {},
|
|
"module_id": None,
|
|
"digest": None,
|
|
"timestamp": None,
|
|
"has_certificate": False,
|
|
"has_cabundle": False,
|
|
"has_public_key": False,
|
|
"issues": [],
|
|
}
|
|
try:
|
|
attestation_bytes = base64.b64decode(attestation_b64)
|
|
|
|
# COSE_Sign1 is a CBOR array: [protected, unprotected, payload, signature]
|
|
cose_structure = cbor2.loads(attestation_bytes)
|
|
if hasattr(cose_structure, "tag") and cose_structure.tag == 18:
|
|
cose_array = cose_structure.value
|
|
elif isinstance(cose_structure, list) and len(cose_structure) == 4:
|
|
cose_array = cose_structure
|
|
else:
|
|
result["issues"].append("Not a valid COSE_Sign1 structure")
|
|
return result
|
|
|
|
payload = cbor2.loads(cose_array[2])
|
|
|
|
result["module_id"] = payload.get("module_id")
|
|
result["digest"] = payload.get("digest")
|
|
result["timestamp"] = payload.get("timestamp")
|
|
|
|
if result["timestamp"]:
|
|
ts = datetime.fromtimestamp(result["timestamp"] / 1000, tz=timezone.utc)
|
|
result["timestamp_human"] = ts.isoformat()
|
|
|
|
pcrs = payload.get("pcrs", {})
|
|
for idx, value in pcrs.items():
|
|
result["pcrs"][f"PCR{idx}"] = value.hex() if isinstance(value, bytes) else str(value)
|
|
|
|
result["has_certificate"] = "certificate" in payload and payload["certificate"] is not None
|
|
result["has_cabundle"] = "cabundle" in payload and len(payload.get("cabundle", [])) > 0
|
|
result["has_public_key"] = "public_key" in payload and payload["public_key"] is not None
|
|
|
|
result["valid_structure"] = True
|
|
|
|
if not result["has_cabundle"]:
|
|
result["issues"].append("Missing CA bundle - cannot verify certificate chain to AWS root")
|
|
if not result["has_public_key"]:
|
|
result["issues"].append("No public key in attestation - KMS cannot encrypt response to enclave")
|
|
if "PCR0" not in result["pcrs"]:
|
|
result["issues"].append("PCR0 (image hash) not present in attestation document")
|
|
|
|
except Exception as e:
|
|
result["issues"].append(f"Attestation parsing error: {str(e)}")
|
|
|
|
return result
|
|
|
|
|
|
def audit_cloudtrail_enclave_events(cloudtrail_client, days_back=7):
|
|
"""Search CloudTrail for enclave-related security events."""
|
|
from datetime import timedelta
|
|
end_time = datetime.now(timezone.utc)
|
|
start_time = end_time - timedelta(days=days_back)
|
|
|
|
events_of_interest = [
|
|
"RunInstances",
|
|
"TerminateInstances",
|
|
"ModifyInstanceAttribute",
|
|
]
|
|
kms_events = ["Decrypt", "GenerateDataKey", "GenerateDataKeyPair", "GenerateRandom"]
|
|
|
|
findings = []
|
|
|
|
# Check for instance launches with enclave options
|
|
for event_name in events_of_interest:
|
|
try:
|
|
response = cloudtrail_client.lookup_events(
|
|
LookupAttributes=[
|
|
{"AttributeKey": "EventName", "AttributeValue": event_name}
|
|
],
|
|
StartTime=start_time,
|
|
EndTime=end_time,
|
|
MaxResults=50,
|
|
)
|
|
for event in response.get("Events", []):
|
|
ct_event = json.loads(event.get("CloudTrailEvent", "{}"))
|
|
req_params = ct_event.get("requestParameters", {})
|
|
|
|
if event_name == "RunInstances":
|
|
enclave_opts = req_params.get("enclaveOptions", {})
|
|
if enclave_opts.get("enabled"):
|
|
findings.append({
|
|
"event": event_name,
|
|
"time": event["EventTime"].isoformat(),
|
|
"user": event.get("Username"),
|
|
"detail": "Enclave-enabled instance launched",
|
|
"source_ip": ct_event.get("sourceIPAddress"),
|
|
})
|
|
except ClientError:
|
|
continue
|
|
|
|
# Check for KMS calls with Recipient parameter (enclave attestation)
|
|
for event_name in kms_events:
|
|
try:
|
|
response = cloudtrail_client.lookup_events(
|
|
LookupAttributes=[
|
|
{"AttributeKey": "EventName", "AttributeValue": event_name}
|
|
],
|
|
StartTime=start_time,
|
|
EndTime=end_time,
|
|
MaxResults=50,
|
|
)
|
|
for event in response.get("Events", []):
|
|
ct_event = json.loads(event.get("CloudTrailEvent", "{}"))
|
|
req_params = ct_event.get("requestParameters", {})
|
|
if "recipient" in req_params or "Recipient" in req_params:
|
|
findings.append({
|
|
"event": event_name,
|
|
"time": event["EventTime"].isoformat(),
|
|
"user": event.get("Username"),
|
|
"detail": "KMS operation with enclave attestation document",
|
|
"key_id": req_params.get("keyId"),
|
|
"source_ip": ct_event.get("sourceIPAddress"),
|
|
})
|
|
except ClientError:
|
|
continue
|
|
|
|
logger.info("Found %d enclave-related CloudTrail events", len(findings))
|
|
return findings
|
|
|
|
|
|
def generate_report(instances, kms_audits, iam_audits, cloudtrail_events, attestation_results=None):
|
|
"""Generate comprehensive Nitro Enclave security assessment report."""
|
|
total_issues = 0
|
|
critical_issues = []
|
|
|
|
for audit in kms_audits:
|
|
total_issues += len(audit.get("issues", []))
|
|
if not audit.get("has_attestation_condition"):
|
|
critical_issues.append(f"KMS key {audit['key_id']} has no attestation conditions")
|
|
|
|
for audit in iam_audits:
|
|
total_issues += len(audit.get("issues", []))
|
|
if audit.get("overprivileged"):
|
|
critical_issues.append(f"IAM role {audit['role_name']} is overprivileged")
|
|
|
|
report = {
|
|
"report_type": "Nitro Enclave Security Assessment",
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"summary": {
|
|
"enclave_instances": len(instances),
|
|
"kms_keys_audited": len(kms_audits),
|
|
"iam_roles_audited": len(iam_audits),
|
|
"cloudtrail_events": len(cloudtrail_events),
|
|
"total_issues": total_issues,
|
|
"critical_issues": len(critical_issues),
|
|
},
|
|
"critical_findings": critical_issues,
|
|
"instances": instances,
|
|
"kms_policy_audits": kms_audits,
|
|
"iam_role_audits": iam_audits,
|
|
"cloudtrail_events": cloudtrail_events,
|
|
}
|
|
|
|
if attestation_results:
|
|
report["attestation_validation"] = attestation_results
|
|
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="AWS Nitro Enclave Security Assessment Agent")
|
|
parser.add_argument("--region", default="us-east-1", help="AWS region")
|
|
parser.add_argument("--kms-key-ids", nargs="+", help="KMS key IDs to audit")
|
|
parser.add_argument("--iam-roles", nargs="+", help="IAM role names to audit for enclave permissions")
|
|
parser.add_argument("--attestation-doc", help="Base64-encoded attestation document to validate")
|
|
parser.add_argument("--cloudtrail-days", type=int, default=7, help="Days of CloudTrail history to search")
|
|
parser.add_argument("--output", default="nitro_enclave_security_report.json", help="Output report file")
|
|
args = parser.parse_args()
|
|
|
|
session = boto3.Session(region_name=args.region)
|
|
ec2_client = session.client("ec2")
|
|
kms_client = session.client("kms")
|
|
iam_client = session.client("iam")
|
|
cloudtrail_client = session.client("cloudtrail")
|
|
|
|
logger.info("Starting Nitro Enclave security assessment in %s", args.region)
|
|
|
|
# Step 1: Find enclave-enabled instances
|
|
instances = get_nitro_instances(ec2_client, args.region)
|
|
|
|
# Step 2: Audit KMS key policies
|
|
kms_audits = []
|
|
if args.kms_key_ids:
|
|
for key_id in args.kms_key_ids:
|
|
logger.info("Auditing KMS key: %s", key_id)
|
|
kms_audits.append(audit_kms_key_policy(kms_client, key_id))
|
|
else:
|
|
# Auto-discover KMS keys
|
|
try:
|
|
keys_response = kms_client.list_keys(Limit=100)
|
|
for key in keys_response.get("Keys", []):
|
|
audit = audit_kms_key_policy(kms_client, key["KeyId"])
|
|
if audit.get("has_attestation_condition") or audit.get("allowed_actions"):
|
|
kms_audits.append(audit)
|
|
except ClientError as e:
|
|
logger.warning("Cannot list KMS keys: %s", e)
|
|
|
|
# Step 3: Audit IAM roles
|
|
iam_audits = []
|
|
if args.iam_roles:
|
|
for role_name in args.iam_roles:
|
|
logger.info("Auditing IAM role: %s", role_name)
|
|
iam_audits.append(audit_iam_role_for_enclave(iam_client, role_name))
|
|
|
|
# Step 4: Search CloudTrail events
|
|
cloudtrail_events = audit_cloudtrail_enclave_events(cloudtrail_client, args.cloudtrail_days)
|
|
|
|
# Step 5: Validate attestation document if provided
|
|
attestation_results = None
|
|
if args.attestation_doc:
|
|
logger.info("Validating attestation document")
|
|
attestation_results = validate_attestation_document_structure(args.attestation_doc)
|
|
|
|
# Generate report
|
|
report = generate_report(instances, kms_audits, iam_audits, cloudtrail_events, attestation_results)
|
|
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2, default=str)
|
|
logger.info("Report saved to %s", args.output)
|
|
|
|
# Print summary
|
|
summary = report["summary"]
|
|
logger.info(
|
|
"Assessment complete: %d instances, %d KMS keys, %d IAM roles, %d issues (%d critical)",
|
|
summary["enclave_instances"],
|
|
summary["kms_keys_audited"],
|
|
summary["iam_roles_audited"],
|
|
summary["total_issues"],
|
|
summary["critical_issues"],
|
|
)
|
|
|
|
if report["critical_findings"]:
|
|
logger.warning("CRITICAL FINDINGS:")
|
|
for finding in report["critical_findings"]:
|
|
logger.warning(" - %s", finding)
|
|
|
|
return 0 if summary["critical_issues"] == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|