#!/usr/bin/env bash
set -euo pipefail

# EC2 Hardening Validation Script
#
# Purpose:
#   Inspect an EC2 instance's identity and network exposure posture without
#   making any changes. This helps validate a hardening model based on:
#   - IAM roles instead of static access keys
#   - Tight security group ingress/egress rules
#   - IMDSv2 awareness
#
# Safe by design:
#   - Read-only checks only
#   - No destructive actions
#   - No secrets, credentials, or environment-specific endpoints
#
# Requirements:
#   - AWS CLI v2 configured with permissions to read EC2 metadata, instance
#     profile associations, and security group details
#   - bash, jq (optional but recommended for cleaner output)
#
# Usage:
#   ./ec2-hardening-validate.sh --instance-id i-0123456789abcdef0
#   ./ec2-hardening-validate.sh --instance-id i-0123456789abcdef0 --region us-east-1
#
# Optional flags:
#   --instance-id   EC2 instance ID to inspect (required)
#   --region        AWS region override
#   --profile       AWS CLI profile name
#   --help          Show this help

usage() {
  cat <<'EOF'
Usage: ec2-hardening-validate.sh --instance-id <i-...> [--region <region>] [--profile <profile>]

Checks:
  - Instance profile / IAM role attachment
  - Metadata service options (IMDS)
  - Security groups attached to the instance
  - Ingress and egress rules for broad exposure

Notes:
  - This script does not modify resources.
  - It flags potential risks for manual review.
EOF
}

log() {
  printf '[%s] %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"
}

warn() {
  printf '[%s] WARNING: %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*" >&2
}

err() {
  printf '[%s] ERROR: %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*" >&2
}

INSTANCE_ID=""
AWS_REGION=""
AWS_PROFILE=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --instance-id)
      INSTANCE_ID="${2:-}"
      shift 2
      ;;
    --region)
      AWS_REGION="${2:-}"
      shift 2
      ;;
    --profile)
      AWS_PROFILE="${2:-}"
      shift 2
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      err "Unknown argument: $1"
      usage
      exit 1
      ;;
  esac
done

if [[ -z "$INSTANCE_ID" ]]; then
  err "--instance-id is required"
  usage
  exit 1
fi

if ! command -v aws >/dev/null 2>&1; then
  err "aws CLI is not installed or not in PATH"
  exit 1
fi

AWS_BASE=(aws)
if [[ -n "$AWS_PROFILE" ]]; then
  AWS_BASE+=(--profile "$AWS_PROFILE")
fi
if [[ -n "$AWS_REGION" ]]; then
  AWS_BASE+=(--region "$AWS_REGION")
fi

aws_cmd() {
  "${AWS_BASE[@]}" "$@"
}

get_instance_json() {
  aws_cmd ec2 describe-instances \
    --instance-ids "$INSTANCE_ID" \
    --output json
}

log "Inspecting instance: $INSTANCE_ID"

INSTANCE_JSON="$(get_instance_json)"

# Extract basic instance data
IAM_INSTANCE_PROFILE_ARN=""
SECURITY_GROUP_IDS=()
SUBNET_ID=""
VPC_ID=""
IMDS_HTTP_TOKENS=""
IMDS_HOP_LIMIT=""

if command -v jq >/dev/null 2>&1; then
  IAM_INSTANCE_PROFILE_ARN="$(printf '%s' "$INSTANCE_JSON" | jq -r '.Reservations[0].Instances[0].IamInstanceProfile.Arn // empty')"
  SUBNET_ID="$(printf '%s' "$INSTANCE_JSON" | jq -r '.Reservations[0].Instances[0].SubnetId // empty')"
  VPC_ID="$(printf '%s' "$INSTANCE_JSON" | jq -r '.Reservations[0].Instances[0].VpcId // empty')"
  mapfile -t SECURITY_GROUP_IDS < <(printf '%s' "$INSTANCE_JSON" | jq -r '.Reservations[0].Instances[0].SecurityGroups[].GroupId')
  IMDS_HTTP_TOKENS="$(printf '%s' "$INSTANCE_JSON" | jq -r '.Reservations[0].Instances[0].MetadataOptions.HttpTokens // empty')"
  IMDS_HOP_LIMIT="$(printf '%s' "$INSTANCE_JSON" | jq -r '.Reservations[0].Instances[0].MetadataOptions.HttpPutResponseHopLimit // empty')"
else
  warn "jq not found; using AWS CLI text output where possible"
  IAM_INSTANCE_PROFILE_ARN="$(aws_cmd ec2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].IamInstanceProfile.Arn' --output text 2>/dev/null || true)"
  SUBNET_ID="$(aws_cmd ec2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].SubnetId' --output text 2>/dev/null || true)"
  VPC_ID="$(aws_cmd ec2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].VpcId' --output text 2>/dev/null || true)"
  IMDS_HTTP_TOKENS="$(aws_cmd ec2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].MetadataOptions.HttpTokens' --output text 2>/dev/null || true)"
  IMDS_HOP_LIMIT="$(aws_cmd ec2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].MetadataOptions.HttpPutResponseHopLimit' --output text 2>/dev/null || true)"
  mapfile -t SECURITY_GROUP_IDS < <(aws_cmd ec2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].SecurityGroups[].GroupId' --output text 2>/dev/null | tr '\t' '\n')
fi

if [[ -z "$IAM_INSTANCE_PROFILE_ARN" || "$IAM_INSTANCE_PROFILE_ARN" == "None" ]]; then
  warn "No IAM instance profile detected. Consider attaching a role instead of using static access keys."
else
  log "IAM instance profile attached: $IAM_INSTANCE_PROFILE_ARN"
fi

if [[ -n "$IMDS_HTTP_TOKENS" ]]; then
  if [[ "$IMDS_HTTP_TOKENS" == "required" ]]; then
    log "IMDSv2 is enforced (HttpTokens=required)"
  else
    warn "IMDSv2 is not enforced (HttpTokens=$IMDS_HTTP_TOKENS). Consider requiring tokens."
  fi
else
  warn "Unable to determine IMDS token setting"
fi

if [[ -n "$IMDS_HOP_LIMIT" ]]; then
  log "IMDS hop limit: $IMDS_HOP_LIMIT"
fi

log "Instance network context: subnet=${SUBNET_ID:-unknown}, vpc=${VPC_ID:-unknown}"

if [[ ${#SECURITY_GROUP_IDS[@]} -eq 0 ]]; then
  warn "No security groups found or unable to read them"
  exit 0
fi

log "Security groups attached: ${SECURITY_GROUP_IDS[*]}"

# Inspect each security group for broad exposure patterns.
for SG_ID in "${SECURITY_GROUP_IDS[@]}"; do
  [[ -z "$SG_ID" || "$SG_ID" == "None" ]] && continue
  log "Reviewing security group: $SG_ID"

  SG_JSON="$(aws_cmd ec2 describe-security-groups --group-ids "$SG_ID" --output json)"

  if command -v jq >/dev/null 2>&1; then
    GROUP_NAME="$(printf '%s' "$SG_JSON" | jq -r '.SecurityGroups[0].GroupName // empty')"
    VPC_ID_SG="$(printf '%s' "$SG_JSON" | jq -r '.SecurityGroups[0].VpcId // empty')"
    log "  Name: ${GROUP_NAME:-unknown} | VPC: ${VPC_ID_SG:-unknown}"

    # Flag broad ingress rules
    printf '%s' "$SG_JSON" | jq -r '
      .SecurityGroups[0].IpPermissions[]? |
      {
        ipProtocol: .IpProtocol,
        fromPort: (.FromPort // "all"),
        toPort: (.ToPort // "all"),
        cidrs: [.IpRanges[]?.CidrIp],
        ipv6: [.Ipv6Ranges[]?.CidrIpv6],
        sgRefs: [.UserIdGroupPairs[]?.GroupId]
      } |
      @tsv' | while IFS=$'\t' read -r proto from_port to_port cidrs ipv6 sgrefs; do
        if [[ "$cidrs" == *"0.0.0.0/0"* ]] || [[ "$ipv6" == *"::/0"* ]]; then
          warn "  Ingress may be overly broad: proto=${proto}, ports=${from_port}-${to_port}, cidrs=${cidrs}, ipv6=${ipv6}"
        fi
      done

    # Flag broad egress rules
    printf '%s' "$SG_JSON" | jq -r '
      .SecurityGroups[0].IpPermissionsEgress[]? |
      {
        ipProtocol: .IpProtocol,
        fromPort: (.FromPort // "all"),
        toPort: (.ToPort // "all"),
        cidrs: [.IpRanges[]?.CidrIp],
        ipv6: [.Ipv6Ranges[]?.CidrIpv6],
        sgRefs: [.UserIdGroupPairs[]?.GroupId]
      } |
      @tsv' | while IFS=$'\t' read -r proto from_port to_port cidrs ipv6 sgrefs; do
        if [[ "$cidrs" == *"0.0.0.0/0"* ]] || [[ "$ipv6" == *"::/0"* ]]; then
          warn "  Egress may be overly broad: proto=${proto}, ports=${from_port}-${to_port}, cidrs=${cidrs}, ipv6=${ipv6}"
        fi
      done
  else
    warn "jq not found; security group content inspection is limited"
    aws_cmd ec2 describe-security-groups --group-ids "$SG_ID" --query 'SecurityGroups[0].{GroupName:GroupName,VpcId:VpcId}' --output table
  fi

done

cat <<'EOF'

Next review steps:
  1. Confirm the attached IAM role only has permissions the workload truly needs.
  2. Confirm inbound rules allow only approved sources and ports.
  3. Confirm outbound rules are no broader than operationally necessary.
  4. Verify application startup, health checks, and dependency access after tightening.
  5. Review CloudTrail and application logs for denied API calls or blocked traffic.
EOF

log "Validation complete"