#!/bin/bash
INPUT="${1:-/mnt/c/Users/my_ch/.claude/20260511_progtu/delete_candidates.txt}"
OUTPUT="/mnt/c/Users/my_ch/.claude/20260511_progtu/aws_cost_estimate_$(date +%Y%m%d_%H%M%S).txt"
exec > >(tee "$OUTPUT") 2>&1
echo "======================================"
echo " AWS コスト試算スクリプト"
echo " 入力ファイル: $INPUT"
echo " 出力ファイル: $OUTPUT"
echo " ※AWS Pricing API から単価を取得"
echo "======================================"
echo ""
printf "%-25s %-45s %-20s %-15s\n" "リソース種類" "リソースID" "計算用情報" "月額削減試算"
echo "-----------------------------------------------------------------------------------------------"
TOTAL=0
# リージョン名 → Pricing API のロケーション名に変換
get_location() {
case "$1" in
ap-northeast-1) echo "Asia Pacific (Tokyo)" ;;
ap-northeast-2) echo "Asia Pacific (Seoul)" ;;
ap-northeast-3) echo "Asia Pacific (Osaka)" ;;
ap-southeast-1) echo "Asia Pacific (Singapore)" ;;
ap-southeast-2) echo "Asia Pacific (Sydney)" ;;
ap-south-1) echo "Asia Pacific (Mumbai)" ;;
us-east-1) echo "US East (N. Virginia)" ;;
us-east-2) echo "US East (Ohio)" ;;
us-west-1) echo "US West (N. California)" ;;
us-west-2) echo "US West (Oregon)" ;;
eu-west-1) echo "Europe (Ireland)" ;;
eu-west-2) echo "Europe (London)" ;;
eu-central-1) echo "Europe (Frankfurt)" ;;
sa-east-1) echo "South America (Sao Paulo)" ;;
ca-central-1) echo "Canada (Central)" ;;
*) echo "Asia Pacific (Tokyo)" ;;
esac
}
# EC2 単価取得($/時間)
get_ec2_price() {
local INSTANCE_TYPE=$1
local LOCATION=$2
aws pricing get-products \
--region us-east-1 \
--service-code AmazonEC2 \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value=$INSTANCE_TYPE" \
"Type=TERM_MATCH,Field=location,Value=$LOCATION" \
"Type=TERM_MATCH,Field=operatingSystem,Value=Linux" \
"Type=TERM_MATCH,Field=tenancy,Value=Shared" \
"Type=TERM_MATCH,Field=capacitystatus,Value=Used" \
"Type=TERM_MATCH,Field=preInstalledSw,Value=NA" \
--query "PriceList[0]" \
--output text 2>/dev/null | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
od = data['terms']['OnDemand']
key1 = list(od.keys())[0]
key2 = list(od[key1]['priceDimensions'].keys())[0]
print(od[key1]['priceDimensions'][key2]['pricePerUnit']['USD'])
" 2>/dev/null
}
# RDS 単価取得($/時間)
get_rds_price() {
local INSTANCE_TYPE=$1
local LOCATION=$2
aws pricing get-products \
--region us-east-1 \
--service-code AmazonRDS \
--filters \
"Type=TERM_MATCH,Field=instanceType,Value=$INSTANCE_TYPE" \
"Type=TERM_MATCH,Field=location,Value=$LOCATION" \
"Type=TERM_MATCH,Field=databaseEngine,Value=MySQL" \
"Type=TERM_MATCH,Field=deploymentOption,Value=Single-AZ" \
--query "PriceList[0]" \
--output text 2>/dev/null | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
od = data['terms']['OnDemand']
key1 = list(od.keys())[0]
key2 = list(od[key1]['priceDimensions'].keys())[0]
print(od[key1]['priceDimensions'][key2]['pricePerUnit']['USD'])
" 2>/dev/null
}
# EBS 単価取得($/GB/月)
get_ebs_price() {
local VOL_TYPE=$1
local LOCATION=$2
local API_VOL_TYPE
case "$VOL_TYPE" in
gp3) API_VOL_TYPE="General Purpose" ;;
gp2) API_VOL_TYPE="General Purpose" ;;
io1) API_VOL_TYPE="Provisioned IOPS" ;;
st1) API_VOL_TYPE="Throughput Optimized HDD" ;;
sc1) API_VOL_TYPE="Cold HDD" ;;
*) API_VOL_TYPE="General Purpose" ;;
esac
aws pricing get-products \
--region us-east-1 \
--service-code AmazonEC2 \
--filters \
"Type=TERM_MATCH,Field=productFamily,Value=Storage" \
"Type=TERM_MATCH,Field=location,Value=$LOCATION" \
"Type=TERM_MATCH,Field=volumeApiName,Value=$VOL_TYPE" \
--query "PriceList[0]" \
--output text 2>/dev/null | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
od = data['terms']['OnDemand']
key1 = list(od.keys())[0]
key2 = list(od[key1]['priceDimensions'].keys())[0]
print(od[key1]['priceDimensions'][key2]['pricePerUnit']['USD'])
" 2>/dev/null
}
while IFS=$'\t' read -r TYPE ID REGION INFO; do
# コメント行・空行スキップ
[[ "$TYPE" =~ ^#.*$ ]] || [ -z "$TYPE" ] && continue
COST=0
LOCATION=$(get_location "$REGION")
case "$TYPE" in
EC2)
HOURLY=$(get_ec2_price "$INFO" "$LOCATION")
[ -z "$HOURLY" ] && HOURLY=0
COST=$(awk "BEGIN {printf \"%.2f\", $HOURLY * 24 * 30}")
;;
EBS)
SIZE=$(echo "$INFO" | cut -d'/' -f1 | tr -d 'GB')
VOL_TYPE=$(echo "$INFO" | cut -d'/' -f2)
PRICE_PER_GB=$(get_ebs_price "$VOL_TYPE" "$LOCATION")
[ -z "$PRICE_PER_GB" ] && PRICE_PER_GB=0
COST=$(awk "BEGIN {printf \"%.2f\", $SIZE * $PRICE_PER_GB}")
;;
RDS)
HOURLY=$(get_rds_price "$INFO" "$LOCATION")
[ -z "$HOURLY" ] && HOURLY=0
COST=$(awk "BEGIN {printf \"%.2f\", $HOURLY * 24 * 30}")
;;
NAT)
# Pricing API 非対応のため固定単価(全リージョンほぼ同額)
COST=$(awk "BEGIN {printf \"%.2f\", 0.062 * 24 * 30}")
;;
EIP)
# Pricing API 非対応のため固定単価
COST=$(awk "BEGIN {printf \"%.2f\", 0.005 * 24 * 30}")
;;
LB)
# Pricing API 非対応のため固定単価
COST=$(awk "BEGIN {printf \"%.2f\", 0.0243 * 24 * 30}")
;;
EC2スナップショット)
# Pricing API 非対応のため固定単価($0.05/GB/月)
SIZE=$(echo "$INFO" | tr -d 'GB')
COST=$(awk "BEGIN {printf \"%.2f\", $SIZE * 0.05}")
;;
RDSスナップショット)
# Pricing API 非対応のため固定単価($0.095/GB/月)
SIZE=$(echo "$INFO" | tr -d 'GB')
COST=$(awk "BEGIN {printf \"%.2f\", $SIZE * 0.095}")
;;
"CloudWatch Logs")
# 保存期間設定で削減(削除ではなく設定変更のため試算対象外)
COST=0
INFO="保存期間設定で対応"
;;
esac
TOTAL=$(awk "BEGIN {printf \"%.2f\", $TOTAL + $COST}")
printf "%-25s %-45s %-20s \$%-15s\n" "$TYPE" "$ID" "$INFO" "$COST"
done < "$INPUT"
echo "-----------------------------------------------------------------------------------------------"
printf "%-25s %-45s %-20s \$%-15s\n" "合計削減試算" "" "" "$TOTAL"
echo ""
echo "======================================"
echo " 完了"
echo "======================================"
#!/bin/bash
OUTPUT="/mnt/c/Users/my_ch/.claude/20260511_progtu/aws_cost_check_$(date +%Y%m%d_%H%M%S).txt"
exec > >(tee "$OUTPUT") 2>&1
START_TIME=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)
END_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "======================================"
echo " AWS リソース洗い出しスクリプト"
echo " 出力ファイル: $OUTPUT"
echo " 調査期間: 過去30日間"
echo "======================================"
echo ""
printf "%-25s %-45s %-20s %-30s %-20s\n" "リソース種類" "リソース名/ID" "リージョン" "未使用判定" "計算用情報"
echo "--------------------------------------------------------------------------------------------------------------------"
REGIONS=$(aws ec2 describe-regions --query "Regions[*].RegionName" --output text)
# S3(グローバル)
# CLI: list-buckets → s3api list-objects-v2 でオブジェクト数・最終更新日を確認
# 判定: 空 = 未使用、最終更新が90日以上前 = 長期未使用の可能性、それ以外 = 使用中
THRESHOLD_DAYS=90
NOW_EPOCH=$(date +%s)
for BUCKET in $(aws s3api list-buckets --query "Buckets[*].Name" --output text); do
COUNT=$(aws s3api list-objects-v2 --bucket "$BUCKET" --query "length(Contents)" --output text 2>/dev/null)
if [ "$COUNT" = "None" ] || [ "$COUNT" = "0" ] || [ -z "$COUNT" ]; then
UNUSED="未使用(空)"
else
LAST_MODIFIED=$(aws s3api list-objects-v2 --bucket "$BUCKET" \
--query "sort_by(Contents, &LastModified)[-1].LastModified" \
--output text 2>/dev/null)
if [ -n "$LAST_MODIFIED" ] && [ "$LAST_MODIFIED" != "None" ]; then
LAST_EPOCH=$(date -d "${LAST_MODIFIED%%.*}" +%s 2>/dev/null)
DAYS=$(( (NOW_EPOCH - LAST_EPOCH) / 86400 ))
if [ "$DAYS" -ge "$THRESHOLD_DAYS" ]; then
UNUSED="要確認(最終更新:${DAYS}日前)"
else
UNUSED="使用中(最終更新:${DAYS}日前)"
fi
else
UNUSED="使用中"
fi
fi
printf "%-25s %-45s %-20s %-30s %-20s\n" "S3バケット" "$BUCKET" "global" "$UNUSED" "${COUNT:-0}オブジェクト"
done
# Route53(グローバル)
# 判定: レコード数が2以下(デフォルトのNSとSOAレコードのみ)= カスタムレコードなし = 未使用の可能性
aws route53 list-hosted-zones \
--query "HostedZones[*].[Name,ResourceRecordSetCount]" \
--output text 2>/dev/null | while IFS=$'\t' read -r NAME COUNT; do
if [ "$COUNT" -le 2 ] 2>/dev/null; then UNUSED="未使用の可能性"; else UNUSED="使用中"; fi
printf "%-25s %-45s %-20s %-30s %-20s\n" "Route53ホストゾーン" "$NAME" "global" "$UNUSED" "-"
done
for REGION in $REGIONS; do
# EC2
# CLI: describe-instances → CloudWatch CPU使用率をインスタンスごと
# 判定: CloudWatch CPU使用率の過去30日平均が5%未満 = ほぼ使われていない = 未使用
aws ec2 describe-instances --region $REGION \
--query "Reservations[*].Instances[*].[InstanceId,State.Name,InstanceType,Tags[?Key=='Name'].Value|[0]]" \
--output text 2>/dev/null | while IFS=$'\t' read -r INSTANCE_ID STATE TYPE NAME; do
CPU=$(aws cloudwatch get-metric-statistics \
--region $REGION \
--namespace AWS/EC2 \
--metric-name CPUUtilization \
--dimensions Name=InstanceId,Value=$INSTANCE_ID \
--start-time $START_TIME --end-time $END_TIME \
--period 2592000 --statistics Average \
--query "Datapoints[0].Average" --output text 2>/dev/null)
[ "$CPU" = "None" ] || [ -z "$CPU" ] && CPU=0
NAME=${NAME:-"(名前なし)"}
if awk "BEGIN {exit !($CPU < 5)}"; then UNUSED="未使用(CPU:${CPU}%)"; else UNUSED="使用中(CPU:${CPU}%)"; fi
printf "%-25s %-45s %-20s %-30s %-20s\n" "EC2" "$INSTANCE_ID($NAME)" "$REGION" "$UNUSED" "$TYPE"
done
# 未使用の EIP
# 判定: AssociationId が null = EC2 にアタッチされていない = 未使用(アタッチなしでも課金される)
aws ec2 describe-addresses --region $REGION \
--query "Addresses[?AssociationId==null].[PublicIp,AllocationId]" \
--output text 2>/dev/null | while IFS=$'\t' read -r IP ALLOC_ID; do
printf "%-25s %-45s %-20s %-30s %-20s\n" "EIP" "$IP" "$REGION" "未使用(未アタッチ)" "-"
done
# 未使用の EBS
# 判定: status が available = EC2 にアタッチされていない = 未使用(存在するだけで課金される)
aws ec2 describe-volumes --region $REGION \
--filters Name=status,Values=available \
--query "Volumes[*].[VolumeId,Size,VolumeType]" \
--output text 2>/dev/null | while IFS=$'\t' read -r VOL_ID SIZE TYPE; do
printf "%-25s %-45s %-20s %-30s %-20s\n" "EBSボリューム" "$VOL_ID" "$REGION" "未使用(未アタッチ)" "${SIZE}GB/$TYPE"
done
# EC2 スナップショット
# 判定: 自分のアカウントが所有するスナップショットを全件表示(古いものは要確認)
# ※自動で未使用判定はできないため作成日を出力して手動確認
aws ec2 describe-snapshots --region $REGION --owner-ids self \
--query "Snapshots[*].[SnapshotId,VolumeSize,StartTime]" \
--output text 2>/dev/null | while IFS=$'\t' read -r SNAP_ID SIZE DATE; do
printf "%-25s %-45s %-20s %-30s %-20s\n" "EC2スナップショット" "$SNAP_ID" "$REGION" "要確認(作成日:${DATE%%T*})" "${SIZE}GB"
done
# AMI
# 判定: 自分のアカウントが所有する AMI を全件表示(古いものは要確認)
# ※使用中かどうかはEC2の起動テンプレート等と照合が必要なため手動確認
aws ec2 describe-images --region $REGION --owners self \
--query "Images[*].[ImageId,Name,CreationDate]" \
--output text 2>/dev/null | while IFS=$'\t' read -r AMI_ID NAME DATE; do
printf "%-25s %-45s %-20s %-30s %-20s\n" "AMI" "$AMI_ID($NAME)" "$REGION" "要確認(作成日:${DATE%%T*})" "-"
done
# NAT Gateway
# CLI: describe-nat-gateways → CloudWatch 送信バイト数をNATごと
# 判定: CloudWatch の送信バイト数(過去30日合計)が0 = 通信なし = 未使用
aws ec2 describe-nat-gateways --region $REGION \
--filter Name=state,Values=available \
--query "NatGateways[*].[NatGatewayId,Tags[?Key=='Name'].Value|[0]]" \
--output text 2>/dev/null | while IFS=$'\t' read -r NAT_ID NAME; do
BYTES=$(aws cloudwatch get-metric-statistics \
--region $REGION \
--namespace AWS/NATGateway \
--metric-name BytesOutToDestination \
--dimensions Name=NatGatewayId,Value=$NAT_ID \
--start-time $START_TIME --end-time $END_TIME \
--period 2592000 --statistics Sum \
--query "Datapoints[0].Sum" --output text 2>/dev/null)
[ "$BYTES" = "None" ] || [ -z "$BYTES" ] && BYTES=0
NAME=${NAME:-"(名前なし)"}
if awk "BEGIN {exit !($BYTES < 1)}"; then UNUSED="未使用(通信なし)"; else UNUSED="使用中"; fi
printf "%-25s %-45s %-20s %-30s %-20s\n" "NATGateway" "$NAT_ID($NAME)" "$REGION" "$UNUSED" "-"
done
# CloudWatch Logs
# 判定: retentionInDays が null = 保存期間が無期限 = ログが溜まり続けてコスト増加の可能性
aws logs describe-log-groups --region $REGION \
--query "logGroups[?retentionInDays==null].[logGroupName,storedBytes]" \
--output text 2>/dev/null | while IFS=$'\t' read -r LOG_NAME BYTES; do
printf "%-25s %-45s %-20s %-30s %-20s\n" "CloudWatch Logs" "$LOG_NAME" "$REGION" "要確認(保存期間無期限)" "-"
done
# RDS
# CLI: describe-db-instances → CloudWatch 接続数をDBごと
# 判定: CloudWatch の接続数(過去30日平均)が1未満 = 接続なし = 未使用
aws rds describe-db-instances --region $REGION \
--query "DBInstances[*].[DBInstanceIdentifier,DBInstanceStatus,DBInstanceClass]" \
--output text 2>/dev/null | while IFS=$'\t' read -r DB_ID STATUS CLASS; do
CONNECTIONS=$(aws cloudwatch get-metric-statistics \
--region $REGION \
--namespace AWS/RDS \
--metric-name DatabaseConnections \
--dimensions Name=DBInstanceIdentifier,Value=$DB_ID \
--start-time $START_TIME --end-time $END_TIME \
--period 2592000 --statistics Average \
--query "Datapoints[0].Average" --output text 2>/dev/null)
[ "$CONNECTIONS" = "None" ] || [ -z "$CONNECTIONS" ] && CONNECTIONS=0
if awk "BEGIN {exit !($CONNECTIONS < 1)}"; then UNUSED="未使用(接続数:${CONNECTIONS})"; else UNUSED="使用中(接続数:${CONNECTIONS})"; fi
printf "%-25s %-45s %-20s %-30s %-20s\n" "RDS" "$DB_ID" "$REGION" "$UNUSED" "$CLASS"
done
# RDS スナップショット
# 判定: 手動スナップショットを全件表示(古いものは要確認)
# ※自動スナップショットは除外(--snapshot-type manual)
aws rds describe-db-snapshots --region $REGION \
--snapshot-type manual \
--query "DBSnapshots[*].[DBSnapshotIdentifier,DBInstanceIdentifier,SnapshotCreateTime,AllocatedStorage]" \
--output text 2>/dev/null | while IFS=$'\t' read -r SNAP_ID DB_ID DATE SIZE; do
printf "%-25s %-45s %-20s %-30s %-20s\n" "RDSスナップショット" "$SNAP_ID" "$REGION" "要確認(作成日:${DATE%%T*})" "${SIZE}GB"
done
# ロードバランサー
# CLI: describe-load-balancers → CloudWatch リクエスト数をLBごと
# 判定: CloudWatch のリクエスト数(過去30日合計)が0 = リクエストなし = 未使用
aws elbv2 describe-load-balancers --region $REGION \
--query "LoadBalancers[*].[LoadBalancerName,State.Code,Type,LoadBalancerArn]" \
--output text 2>/dev/null | while IFS=$'\t' read -r LB_NAME STATE TYPE LB_ARN; do
REQUESTS=$(aws cloudwatch get-metric-statistics \
--region $REGION \
--namespace AWS/ApplicationELB \
--metric-name RequestCount \
--dimensions Name=LoadBalancer,Value=$(echo $LB_ARN | sed 's/.*loadbalancer\///') \
--start-time $START_TIME --end-time $END_TIME \
--period 2592000 --statistics Sum \
--query "Datapoints[0].Sum" --output text 2>/dev/null)
[ "$REQUESTS" = "None" ] || [ -z "$REQUESTS" ] && REQUESTS=0
if awk "BEGIN {exit !($REQUESTS < 1)}"; then UNUSED="未使用(リクエスト:${REQUESTS})"; else UNUSED="使用中(リクエスト:${REQUESTS})"; fi
printf "%-25s %-45s %-20s %-30s %-20s\n" "ロードバランサー" "$LB_NAME" "$REGION" "$UNUSED" "$TYPE"
done
# ECR
# CLI: describe-repositories → list-images をリポジトリごと
# 判定: リポジトリ内のイメージ数が0 = イメージなし = 未使用
aws ecr describe-repositories --region $REGION \
--query "repositories[*].[repositoryName]" \
--output text 2>/dev/null | while read -r REPO_NAME; do
IMAGE_COUNT=$(aws ecr list-images --region $REGION \
--repository-name $REPO_NAME \
--query "length(imageIds)" --output text 2>/dev/null)
if [ "$IMAGE_COUNT" = "0" ]; then UNUSED="未使用(イメージなし)"; else UNUSED="使用中(${IMAGE_COUNT}イメージ)"; fi
printf "%-25s %-45s %-20s %-30s %-20s\n" "ECRリポジトリ" "$REPO_NAME" "$REGION" "$UNUSED" "-"
done
# Secrets Manager
# 判定: LastAccessedDate が null = 一度もアクセスされていない = 未使用の可能性
aws secretsmanager list-secrets --region $REGION \
--query "SecretList[*].[Name,LastAccessedDate]" \
--output text 2>/dev/null | while IFS=$'\t' read -r SECRET_NAME LAST_ACCESS; do
[ -z "$LAST_ACCESS" ] && UNUSED="未使用(アクセス履歴なし)" || UNUSED="要確認(最終:${LAST_ACCESS%%T*})"
printf "%-25s %-45s %-20s %-30s %-20s\n" "SecretsManager" "$SECRET_NAME" "$REGION" "$UNUSED" "-"
done
# VPN
# 判定: state が available = 起動中 = 存在するだけで課金されるため要確認
aws ec2 describe-vpn-connections --region $REGION \
--filters Name=state,Values=available \
--query "VpnConnections[*].[VpnConnectionId,Tags[?Key=='Name'].Value|[0]]" \
--output text 2>/dev/null | while IFS=$'\t' read -r VPN_ID NAME; do
NAME=${NAME:-"(名前なし)"}
printf "%-25s %-45s %-20s %-30s %-20s\n" "VPN接続" "$VPN_ID($NAME)" "$REGION" "要確認(起動中)" "-"
done
done
echo ""
echo "======================================"
echo " 完了"
echo "======================================"
#!/bin/bash
# 使い方:
# ./aws_iam_audit.sh # 通常実行
# ./aws_iam_audit.sh --mock <csvファイル> # モックCSVで実行(テスト用)
# THRESHOLD=0 ./aws_iam_audit.sh # しきい値を0日に変更して実行
MOCK_FILE=""
if [ "$1" = "--mock" ] && [ -n "$2" ]; then
MOCK_FILE="$2"
fi
OUTPUT="/mnt/c/Users/my_ch/.claude/20260511_progtu/aws_iam_audit_$(date +%Y%m%d_%H%M%S).txt"
exec > >(tee "$OUTPUT") 2>&1
THRESHOLD="${THRESHOLD:-90}" # 長期未使用・古いキー判定のしきい値(日数)。環境変数で上書き可能
TODAY=$(date +%s)
echo "======================================"
echo " AWS IAM 棚卸しスクリプト"
echo " 出力ファイル: $OUTPUT"
echo " しきい値: ${THRESHOLD}日"
[ -n "$MOCK_FILE" ] && echo " モード: モック ($MOCK_FILE)"
echo "======================================"
echo ""
printf "%-20s %-12s %-18s %-18s %-10s %-5s %-22s %-22s %-20s %-35s %-s\n" \
"ユーザー名" "作成日" "最終ログイン" "PW最終変更" "コンソール" "MFA" \
"キー1(状態/最終使用)" "キー2(状態/最終使用)" "所属グループ" "アタッチポリシー" "対応要否"
echo "$(printf '%0.s-' {1..220})"
if [ -n "$MOCK_FILE" ]; then
# モックCSVをそのまま使う
REPORT=$(cat "$MOCK_FILE")
else
# クレデンシャルレポート生成リクエスト
aws iam generate-credential-report --output text > /dev/null 2>&1
# レポートが完成するまで待機(最大30秒)
for i in $(seq 1 10); do
STATE=$(aws iam generate-credential-report --query State --output text 2>/dev/null)
[ "$STATE" = "COMPLETE" ] && break
sleep 3
done
# レポート取得・デコード
REPORT=$(aws iam get-credential-report --query Content --output text 2>/dev/null | base64 -d)
fi
# ヘッダー行スキップして1行ずつ処理(fd3を使いstdinとCSVを分離)
while IFS=',' read -r \
USER ARN CREATION PASSWORD_ENABLED PASSWORD_LAST_USED PASSWORD_LAST_CHANGED \
PASSWORD_NEXT_ROTATION MFA_ACTIVE \
KEY1_ACTIVE KEY1_LAST_ROTATED KEY1_LAST_USED_DATE KEY1_LAST_USED_REGION KEY1_LAST_USED_SERVICE \
KEY2_ACTIVE KEY2_LAST_ROTATED KEY2_LAST_USED_DATE KEY2_LAST_USED_REGION KEY2_LAST_USED_SERVICE \
CERT1_ACTIVE CERT1_LAST_ROTATED CERT2_ACTIVE CERT2_LAST_ROTATED <&3; do
# rootアカウントの表示名
[ "$USER" = "<root_account>" ] && USER="(root)"
# --- 作成日 ---
CREATED="${CREATION%%T*}"
# --- 最終ログイン ---
if [ "$PASSWORD_LAST_USED" = "N/A" ] || [ "$PASSWORD_LAST_USED" = "no_information" ] || [ -z "$PASSWORD_LAST_USED" ]; then
LAST_LOGIN="未ログイン"
LAST_LOGIN_DAYS=9999
else
LAST_LOGIN_DATE="${PASSWORD_LAST_USED%%T*}"
LAST_LOGIN_EPOCH=$(date -d "$LAST_LOGIN_DATE" +%s 2>/dev/null || echo 0)
LAST_LOGIN_DAYS=$(( (TODAY - LAST_LOGIN_EPOCH) / 86400 ))
LAST_LOGIN="${LAST_LOGIN_DATE}(${LAST_LOGIN_DAYS}日前)"
fi
# --- PW最終変更 ---
if [ "$PASSWORD_LAST_CHANGED" = "N/A" ] || [ "$PASSWORD_LAST_CHANGED" = "not_supported" ] || [ -z "$PASSWORD_LAST_CHANGED" ]; then
PW_CHANGED="N/A"
PW_CHANGED_DAYS=0
else
PW_CHANGED_DATE="${PASSWORD_LAST_CHANGED%%T*}"
PW_CHANGED_EPOCH=$(date -d "$PW_CHANGED_DATE" +%s 2>/dev/null || echo 0)
PW_CHANGED_DAYS=$(( (TODAY - PW_CHANGED_EPOCH) / 86400 ))
PW_CHANGED="${PW_CHANGED_DATE}(${PW_CHANGED_DAYS}日前)"
fi
# --- コンソールアクセス ---
[ "$PASSWORD_ENABLED" = "true" ] && CONSOLE="有効" || CONSOLE="無効"
# --- MFA ---
[ "$MFA_ACTIVE" = "true" ] && MFA="有" || MFA="無"
# --- アクセスキー1 ---
KEY1_ROT_DAYS=0
KEY1_LAST_DAYS=0
if [ "$KEY1_ACTIVE" = "true" ] || [ "$KEY1_ACTIVE" = "false" ]; then
KEY1_STATE=$([ "$KEY1_ACTIVE" = "true" ] && echo "有効" || echo "無効")
# キー作成からの経過日数
if [ "$KEY1_LAST_ROTATED" = "N/A" ] || [ -z "$KEY1_LAST_ROTATED" ]; then
KEY1_ROT_DAYS=0
KEY1_ROT_LABEL="作成日不明"
else
KEY1_ROT_DATE="${KEY1_LAST_ROTATED%%T*}"
KEY1_ROT_EPOCH=$(date -d "$KEY1_ROT_DATE" +%s 2>/dev/null || echo 0)
KEY1_ROT_DAYS=$(( (TODAY - KEY1_ROT_EPOCH) / 86400 ))
KEY1_ROT_LABEL="作成${KEY1_ROT_DAYS}日前"
fi
# キー最終使用日
if [ "$KEY1_LAST_USED_DATE" = "N/A" ] || [ -z "$KEY1_LAST_USED_DATE" ]; then
KEY1_LAST_DAYS=9999
KEY1="${KEY1_STATE}/未使用/${KEY1_ROT_LABEL}"
else
KEY1_DATE="${KEY1_LAST_USED_DATE%%T*}"
KEY1_EPOCH=$(date -d "$KEY1_DATE" +%s 2>/dev/null || echo 0)
KEY1_LAST_DAYS=$(( (TODAY - KEY1_EPOCH) / 86400 ))
KEY1="${KEY1_STATE}/最終:${KEY1_DATE}(${KEY1_LAST_DAYS}日前)/${KEY1_ROT_LABEL}"
fi
else
KEY1="なし"
fi
# --- アクセスキー2 ---
KEY2_ROT_DAYS=0
KEY2_LAST_DAYS=0
if [ "$KEY2_ACTIVE" = "true" ] || [ "$KEY2_ACTIVE" = "false" ]; then
KEY2_STATE=$([ "$KEY2_ACTIVE" = "true" ] && echo "有効" || echo "無効")
if [ "$KEY2_LAST_ROTATED" = "N/A" ] || [ -z "$KEY2_LAST_ROTATED" ]; then
KEY2_ROT_DAYS=0
KEY2_ROT_LABEL="作成日不明"
else
KEY2_ROT_DATE="${KEY2_LAST_ROTATED%%T*}"
KEY2_ROT_EPOCH=$(date -d "$KEY2_ROT_DATE" +%s 2>/dev/null || echo 0)
KEY2_ROT_DAYS=$(( (TODAY - KEY2_ROT_EPOCH) / 86400 ))
KEY2_ROT_LABEL="作成${KEY2_ROT_DAYS}日前"
fi
if [ "$KEY2_LAST_USED_DATE" = "N/A" ] || [ -z "$KEY2_LAST_USED_DATE" ]; then
KEY2_LAST_DAYS=9999
KEY2="${KEY2_STATE}/未使用/${KEY2_ROT_LABEL}"
else
KEY2_DATE="${KEY2_LAST_USED_DATE%%T*}"
KEY2_EPOCH=$(date -d "$KEY2_DATE" +%s 2>/dev/null || echo 0)
KEY2_LAST_DAYS=$(( (TODAY - KEY2_EPOCH) / 86400 ))
KEY2="${KEY2_STATE}/最終:${KEY2_DATE}(${KEY2_LAST_DAYS}日前)/${KEY2_ROT_LABEL}"
fi
else
KEY2="なし"
fi
# --- 所属グループ / アタッチポリシー(rootは対象外)---
if [ "$USER" = "(root)" ]; then
USER_GROUPS="-"
POLICIES="-"
else
USER_GROUPS=$(aws iam list-groups-for-user --user-name "$USER" \
--query "Groups[*].GroupName" --output text </dev/null 2>/dev/null | tr '\t' ',' | grep -v '^None$')
[ -z "$USER_GROUPS" ] && USER_GROUPS="なし"
# マネージドポリシー
POLICIES=$(aws iam list-attached-user-policies --user-name "$USER" \
--query "AttachedPolicies[*].PolicyName" --output text </dev/null 2>/dev/null | tr '\t' ',' | grep -v '^None$')
# インラインポリシー
INLINE=$(aws iam list-user-policies --user-name "$USER" \
--query "PolicyNames" --output text </dev/null 2>/dev/null | tr '\t' ',' | grep -v '^None$')
[ -n "$INLINE" ] && POLICIES="${POLICIES:+$POLICIES,}${INLINE}(inline)"
[ -z "$POLICIES" ] && POLICIES="なし"
fi
# --- 対応要否判定 ---
ACTIONS=""
# MFA未設定 かつ コンソールアクセス有効
if [ "$MFA_ACTIVE" = "false" ] && [ "$PASSWORD_ENABLED" = "true" ]; then
ACTIONS="${ACTIONS:+$ACTIONS / }要対応:MFA未設定"
fi
# コンソール有効なのに一度もログインしていない
if [ "$LAST_LOGIN" = "未ログイン" ] && [ "$PASSWORD_ENABLED" = "true" ]; then
ACTIONS="${ACTIONS:+$ACTIONS / }要確認:未ログイン"
fi
# 最終ログインがしきい値超え
if [ "$LAST_LOGIN_DAYS" -gt "$THRESHOLD" ] && [ "$LAST_LOGIN" != "未ログイン" ]; then
ACTIONS="${ACTIONS:+$ACTIONS / }要対応:長期未使用(${LAST_LOGIN_DAYS}日)"
fi
# キー1の作成からしきい値超え(古いキー)
if [ "$KEY1_ROT_DAYS" -gt "$THRESHOLD" ] && [ "$KEY1" != "なし" ]; then
ACTIONS="${ACTIONS:+$ACTIONS / }要対応:キー1古い(${KEY1_ROT_DAYS}日)"
fi
# キー2の作成からしきい値超え
if [ "$KEY2_ROT_DAYS" -gt "$THRESHOLD" ] && [ "$KEY2" != "なし" ]; then
ACTIONS="${ACTIONS:+$ACTIONS / }要対応:キー2古い(${KEY2_ROT_DAYS}日)"
fi
# 管理者権限チェック
if echo "$POLICIES" | grep -qi "AdministratorAccess"; then
ACTIONS="${ACTIONS:+$ACTIONS / }要確認:管理者権限"
fi
[ -z "$ACTIONS" ] && ACTIONS="問題なし"
printf "%-20s %-12s %-18s %-18s %-10s %-5s %-22s %-22s %-20s %-35s %-s\n" \
"$USER" "$CREATED" "$LAST_LOGIN" "$PW_CHANGED" "$CONSOLE" "$MFA" \
"$KEY1" "$KEY2" "$USER_GROUPS" "$POLICIES" "$ACTIONS"
done 3< <(echo "$REPORT" | tail -n +2)
echo ""
echo "======================================"
echo " 完了"
echo "======================================"
コメント