Lucene search
+L

AWS Security Posture

🗓️ 02 May 2026 21:27:51Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 3 Views

Audit AWS account security posture for IAM, root safeguards, MFA, and S3 public exposure.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from kittysploit import *
import json


class Module(Post):
    __info__ = {
        "name": "AWS Security Posture",
        "description": "Audit AWS account security posture (IAM, root safeguards, MFA, and S3 public exposure)",
        "author": "KittySploit Team",
        "session_type": SessionType.AWS,
    'agent': {
        'risk': 'intrusive',
        'effects': ['active_exploitation'],
        'expected_requests': 2,
        'reversible': False,
        'approval_required': True,
        'produces': ['risk_signals'],
        'cost': 1.5,
        'noise': 0.5,
        'value': 1.0,
        'requires':         {'min_endpoints': 0,
         'min_params': 0,
         'tech_hints_any': [],
         'tech_hints_all': [],
         'specializations_any': [],
         'risk_signals_any': [],
         'auth_session': False,
         'capabilities_any': [],
         'capabilities_all': [],
         'confidence_min': {},
         'confidence_min_any': {},
         'endpoint_pattern_any': [],
         'param_any': [],
         'api_surface_ready': False},
        'chain':         {'produces_capabilities': [],
         'consumes_capabilities': [],
         'option_bindings': {},
         'suggested_followups': []},
    },
    }

    check_iam_summary = OptBool(True, "Check IAM account summary and root safeguards", False)
    check_password_policy = OptBool(True, "Check IAM password policy", False)
    check_users_mfa = OptBool(True, "Check IAM users without MFA", False)
    check_s3_exposure = OptBool(True, "Check S3 bucket public exposure indicators", False)
    max_users = OptString("30", "Maximum IAM users to inspect for MFA checks", False)
    max_buckets = OptString("30", "Maximum S3 buckets to inspect for exposure checks", False)
    verbose = OptBool(False, "Show additional diagnostic output", False)

    def run(self):
        try:
            print_info("Starting AWS security posture audit...")
            identity = self._get_caller_identity()
            if identity:
                print_info(f"Caller identity: {identity.get('Arn', 'Unknown')}")
                print_info(f"Account: {identity.get('Account', 'Unknown')}")
            else:
                print_warning("Could not resolve caller identity (AWS CLI/boto credentials may be missing)")

            print_info("=" * 80)

            if self.check_iam_summary:
                self._check_iam_summary()
            if self.check_password_policy:
                self._check_password_policy()
            if self.check_users_mfa:
                self._check_users_mfa()
            if self.check_s3_exposure:
                self._check_s3_exposure()

            print_info("=" * 80)
            print_success("AWS security posture audit completed")
            return True
        except Exception as e:
            print_error(f"Error during AWS posture audit: {e}")
            return False

    def _to_int(self, value, default):
        try:
            return max(1, int(str(value).strip()))
        except Exception:
            return default

    def _run_cmd(self, cmd):
        return (self.cmd_execute(cmd) or "").strip()

    def _aws_json(self, cmd):
        output = self._run_cmd(cmd)
        if not output:
            return None
        try:
            return json.loads(output)
        except Exception:
            if self.verbose:
                print_warning(f"Non-JSON output for command: {cmd}")
                print_info(output)
            return None

    def _get_caller_identity(self):
        return self._aws_json("aws sts get-caller-identity 2>/dev/null")

    def _check_iam_summary(self):
        print_status("Check: IAM account summary")
        summary = self._aws_json("aws iam get-account-summary 2>/dev/null")
        if not summary:
            print_warning("Could not read IAM account summary")
            print_info("-" * 80)
            return

        m = summary.get("SummaryMap", {})
        users = m.get("Users", 0)
        roles = m.get("Roles", 0)
        groups = m.get("Groups", 0)
        policies = m.get("Policies", 0)
        root_mfa = m.get("AccountMFAEnabled", 0)
        root_keys = m.get("AccountAccessKeysPresent", 0)

        print_info(f"Users={users}, Roles={roles}, Groups={groups}, CustomerPolicies={policies}")

        if root_mfa == 1:
            print_success("Root account MFA is enabled")
        else:
            print_error("Root account MFA is NOT enabled")

        if root_keys == 0:
            print_success("No root access key present")
        else:
            print_error("Root access key is present")

        print_info("-" * 80)

    def _check_password_policy(self):
        print_status("Check: IAM password policy")
        policy_data = self._aws_json("aws iam get-account-password-policy 2>/dev/null")
        if not policy_data:
            print_warning("No account password policy found or access denied")
            print_info("-" * 80)
            return

        policy = policy_data.get("PasswordPolicy", {})
        min_len = policy.get("MinimumPasswordLength", 0)
        reuse_prevention = policy.get("PasswordReusePrevention", 0)
        max_age = policy.get("MaxPasswordAge", 0)
        hard_expiry = policy.get("HardExpiry", False)
        require_symbols = policy.get("RequireSymbols", False)
        require_numbers = policy.get("RequireNumbers", False)
        require_upper = policy.get("RequireUppercaseCharacters", False)
        require_lower = policy.get("RequireLowercaseCharacters", False)
        mfa_required = policy.get("RequireMFA", None)

        print_info(f"MinimumPasswordLength={min_len}")
        if min_len >= 12:
            print_success("Password minimum length is >= 12")
        else:
            print_warning("Password minimum length is weak (< 12)")

        complexity = all([require_symbols, require_numbers, require_upper, require_lower])
        if complexity:
            print_success("Password complexity requirements are enabled")
        else:
            print_warning("Password complexity requirements are not fully enabled")

        if reuse_prevention and int(reuse_prevention) >= 5:
            print_success(f"PasswordReusePrevention={reuse_prevention}")
        else:
            print_warning(f"PasswordReusePrevention={reuse_prevention} (recommended >= 5)")

        if max_age and int(max_age) <= 90:
            print_success(f"MaxPasswordAge={max_age} days")
        else:
            print_warning(f"MaxPasswordAge={max_age} (recommended <= 90)")

        if hard_expiry:
            print_warning("HardExpiry is enabled (can impact operational access workflows)")
        elif self.verbose:
            print_info("HardExpiry is disabled")

        if mfa_required is not None:
            if mfa_required:
                print_success("Console password policy indicates MFA required")
            else:
                print_warning("Console password policy does not enforce MFA")

        print_info("-" * 80)

    def _check_users_mfa(self):
        print_status("Check: IAM users MFA coverage")
        data = self._aws_json("aws iam list-users 2>/dev/null")
        if not data:
            print_warning("Could not list IAM users")
            print_info("-" * 80)
            return

        users = data.get("Users", [])
        if not users:
            print_success("No IAM users found")
            print_info("-" * 80)
            return

        max_users = self._to_int(self.max_users, 30)
        sample = users[:max_users]
        without_mfa = []

        for u in sample:
            username = u.get("UserName")
            if not username:
                continue
            mfa = self._aws_json(f"aws iam list-mfa-devices --user-name {username} 2>/dev/null")
            devices = (mfa or {}).get("MFADevices", [])
            if len(devices) == 0:
                without_mfa.append(username)

        print_info(f"Scanned {len(sample)}/{len(users)} IAM users for MFA")
        if without_mfa:
            print_warning(f"Users without MFA ({len(without_mfa)}):")
            for name in without_mfa[:20]:
                print_info(f"  - {name}")
            if len(without_mfa) > 20:
                print_info(f"  ... and {len(without_mfa) - 20} more")
        else:
            print_success("All scanned IAM users have MFA devices")

        print_info("-" * 80)

    def _check_s3_exposure(self):
        print_status("Check: S3 public exposure indicators")
        data = self._aws_json("aws s3api list-buckets 2>/dev/null")
        if not data:
            print_warning("Could not list S3 buckets")
            print_info("-" * 80)
            return

        buckets = data.get("Buckets", [])
        if not buckets:
            print_success("No S3 buckets found")
            print_info("-" * 80)
            return

        max_buckets = self._to_int(self.max_buckets, 30)
        sample = buckets[:max_buckets]
        risky = []

        for b in sample:
            name = b.get("Name")
            if not name:
                continue

            pab = self._aws_json(
                f"aws s3api get-public-access-block --bucket {name} 2>/dev/null"
            )
            acl = self._aws_json(f"aws s3api get-bucket-acl --bucket {name} 2>/dev/null")

            pab_cfg = (pab or {}).get("PublicAccessBlockConfiguration", {})
            block_public = (
                pab_cfg.get("BlockPublicAcls", False)
                and pab_cfg.get("IgnorePublicAcls", False)
                and pab_cfg.get("BlockPublicPolicy", False)
                and pab_cfg.get("RestrictPublicBuckets", False)
            )

            public_grant = False
            grants = (acl or {}).get("Grants", [])
            for grant in grants:
                grantee = grant.get("Grantee", {})
                uri = grantee.get("URI", "")
                if "AllUsers" in uri or "AuthenticatedUsers" in uri:
                    public_grant = True
                    break

            if (not block_public) or public_grant:
                risky.append({
                    "bucket": name,
                    "block_public_access_fully_enabled": block_public,
                    "public_acl_grant_detected": public_grant
                })

        print_info(f"Scanned {len(sample)}/{len(buckets)} buckets")
        if risky:
            print_warning(f"Buckets with potential exposure ({len(risky)}):")
            for item in risky[:20]:
                print_info(
                    f"  - {item['bucket']} | block_public_access={item['block_public_access_fully_enabled']} "
                    f"| public_acl={item['public_acl_grant_detected']}"
                )
            if len(risky) > 20:
                print_info(f"  ... and {len(risky) - 20} more")
        else:
            print_success("No obvious S3 public exposure found in scanned buckets")

        print_info("-" * 80)

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Jul 2026 11:31Current
5.8Medium risk
Vulners AI Score5.8
3