Loading_
Loading_
Checks every bucket for public ACLs, permissive policies, disabled block-public-access, missing encryption and absent versioning — exit code 1 on any critical finding, so it drops into CI.
#!/usr/bin/env bash## S3 public exposure scan. Read-only.# Exits 1 if any CRITICAL finding is present, so it can gate a pipeline.## Requires: awscli v2, jq# Usage: ./s3_exposure_scan.sh [--profile NAME] [--quiet] set -euo pipefail PROFILE_ARG=""QUIET=0 while [[ $# -gt 0 ]]; do case "$1" in --profile) PROFILE_ARG="--profile $2"; shift 2 ;; --quiet) QUIET=1; shift ;; *) echo "Unknown argument: $1" >&2; exit 2 ;; esacdone command -v jq >/dev/null || { echo "jq is required" >&2; exit 2; } CRITICAL=0HIGH=0CHECKED=0 RED=$'\033[0;31m'; YEL=$'\033[0;33m'; GRN=$'\033[0;32m'; DIM=$'\033[2m'; OFF=$'\033[0m' finding() { local severity="$1" bucket="$2" message="$3" case "$severity" in CRITICAL) CRITICAL=$((CRITICAL + 1)); printf '%s[CRITICAL]%s %-42s %s\n' "$RED" "$OFF" "$bucket" "$message" ;; HIGH) HIGH=$((HIGH + 1)); printf '%s[HIGH] %s %-42s %s\n' "$YEL" "$OFF" "$bucket" "$message" ;; esac} aws_s3() { aws $PROFILE_ARG "$@" 2>/dev/null; } # Account-level block public accessACCOUNT_ID=$(aws_s3 sts get-caller-identity --query Account --output text)ACCOUNT_BPA=$(aws_s3 s3control get-public-access-block --account-id "$ACCOUNT_ID" \ --query 'PublicAccessBlockConfiguration' --output json || echo '{}') if [[ "$(echo "$ACCOUNT_BPA" | jq -r '.BlockPublicAcls // false')" != "true" ]]; then finding CRITICAL "(account $ACCOUNT_ID)" "Account-level BlockPublicAcls is OFF"fi BUCKETS=$(aws_s3 s3api list-buckets --query 'Buckets[].Name' --output text) for bucket in $BUCKETS; do CHECKED=$((CHECKED + 1)) [[ $QUIET -eq 0 ]] && printf '%s scanning %s%s\n' "$DIM" "$bucket" "$OFF" # 1. Per-bucket block public access BPA=$(aws_s3 s3api get-public-access-block --bucket "$bucket" \ --query 'PublicAccessBlockConfiguration' --output json || echo '{}') for setting in BlockPublicAcls IgnorePublicAcls BlockPublicPolicy RestrictPublicBuckets; do if [[ "$(echo "$BPA" | jq -r --arg k "$setting" '.[$k] // false')" != "true" ]]; then finding HIGH "$bucket" "$setting is not enabled" fi done # 2. ACL grants to AllUsers / AuthenticatedUsers GRANTS=$(aws_s3 s3api get-bucket-acl --bucket "$bucket" \ --query 'Grants[?Grantee.URI!=null].[Grantee.URI,Permission]' --output text || true) if grep -q 'AllUsers' <<<"$GRANTS"; then finding CRITICAL "$bucket" "ACL grants access to AllUsers (public)" fi if grep -q 'AuthenticatedUsers' <<<"$GRANTS"; then finding CRITICAL "$bucket" "ACL grants access to any authenticated AWS principal" fi # 3. Bucket policy with wildcard principal POLICY=$(aws_s3 s3api get-bucket-policy --bucket "$bucket" --query Policy --output text || echo "") if [[ -n "$POLICY" && "$POLICY" != "None" ]]; then WILDCARD=$(echo "$POLICY" | jq -r ' [.Statement[] | select(.Effect == "Allow") | select((.Principal == "*") or (.Principal.AWS? == "*") or ((.Principal.AWS? | type == "array") and (.Principal.AWS | index("*")))) ] | length') if [[ "$WILDCARD" -gt 0 ]]; then HAS_CONDITION=$(echo "$POLICY" | jq -r '[.Statement[] | select(.Condition != null)] | length') if [[ "$HAS_CONDITION" -eq 0 ]]; then finding CRITICAL "$bucket" "Policy allows Principal '*' with no Condition" else finding HIGH "$bucket" "Policy allows Principal '*' (guarded by a Condition - verify it)" fi fi fi # 4. Default encryption if ! aws_s3 s3api get-bucket-encryption --bucket "$bucket" >/dev/null; then finding HIGH "$bucket" "No default encryption configured" fi # 5. Versioning (recovery from ransomware / accidental delete) VERSIONING=$(aws_s3 s3api get-bucket-versioning --bucket "$bucket" --query Status --output text || echo "None") if [[ "$VERSIONING" != "Enabled" ]]; then finding HIGH "$bucket" "Versioning is $VERSIONING" fidone echoprintf 'Scanned %d buckets: %s%d critical%s, %s%d high%s\n' \ "$CHECKED" "$RED" "$CRITICAL" "$OFF" "$YEL" "$HIGH" "$OFF" if [[ $CRITICAL -gt 0 ]]; then echo "Failing build on critical findings." >&2 exit 1fi printf '%sNo critical exposure found.%s\n' "$GRN" "$OFF"Bucket exposure is rarely one setting. A bucket can have block-public-access on at the account level but a policy that grants a wildcard principal, or an ACL granting AllUsers that the console no longer surfaces prominently.
This checks all five layers per bucket and prints a single line per finding, then exits non-zero if anything critical was found so it can gate a pipeline rather than just producing another report nobody reads.
It is read-only and uses only the AWS CLI and jq, so it runs anywhere the CLI is already configured — including inside a CodeBuild step with a read-only role.
| Name | Type | Required | Description |
|---|---|---|---|
--profile | string | Optional | Named AWS CLI profile. |
--quiet | flag | Optional | Suppress per-bucket progress output. |
The platform turns any script into a governed automation — versioned, gated, audited and reversible.