DEV Community

Bims-creator
Bims-creator

Posted on

I audited Prowler's 48 IAM checks against a linter I built, and found a real gap

A few weeks ago I was prepping for a Cloud Security Engineer interview and built a small exercise: a Python function that scans an AWS IAM policy document and flags a few classic risks: wildcard actions, wildcard resources, unrestricted iam:PassRole. It was meant to be throwaway interview prep.

It didn't stay throwaway. I kept adding rules, and it turned into iam-lint, a real, tested, CLI-installable IAM policy scanner. Then, wanting to make sure I wasn't just reinventing something that already existed, I sat down and read through all 48 of Prowler's existing IAM checks. That comparison turned up a genuine, non-trivial gap, which is really the interesting part of this post.

What iam-lint checks

Seven rules, each backed by tests:

Rule Severity Detects
FULL_ADMIN_ACCESS critical Wildcard Action and Resource together in one statement
PRIVILEGE_ESCALATION_RISK critical iam:CreateAccessKey, iam:AttachUserPolicy, iam:PutUserPolicy, or sts:AssumeRole granted without a resource restriction
WILDCARD_PRINCIPAL critical Trust/resource-based policy Principal is "*", granting access to anyone
WILDCARD_ACTION high Action includes "*"
WILDCARD_RESOURCE high Resource includes "*"
PASSROLE_UNRESTRICTED high iam:PassRole not scoped to a specific role ARN
MISSING_MFA_CONDITION medium Sensitive actions allowed without requiring aws:MultiFactorAuthPresent

Run it against a policy, and you get something like this:

$ iam-lint scan policy.json
[HIGH] [WILDCARD_ACTION] statement 0: Action includes "*", granting every action.
[HIGH] [WILDCARD_RESOURCE] statement 0: Resource includes "*", applying account-wide.
[CRITICAL] [FULL_ADMIN_ACCESS] statement 0: Action and Resource are both "*", granting unrestricted admin access.
Enter fullscreen mode Exit fullscreen mode

It exits with code 1 if there are findings and 0 if the policy's clean, so it drops straight into a CI pipeline as a policy gate. There's also a --format json flag for anything more sophisticated that wants machine-readable output.

Under the hood, most of these rules follow the same shape. You normalize a field that AWS lets be either a string or a list (Action, Resource, and Principal all do this), then check it against a wildcard or a small set of risky values. The one that took the most thought was MISSING_MFA_CONDITION, because you're not looking at Action or Resource at all. You're reaching into the statement's Condition block instead:

def has_mfa_condition(statement):
    condition = statement.get("Condition")
    if not isinstance(condition, dict):
        return False
    for operator_block in condition.values():
        if isinstance(operator_block, dict) and "aws:MultiFactorAuthPresent" in operator_block:
            return True
    return False
Enter fullscreen mode Exit fullscreen mode

That little function is what led to the interesting part of this post.

Checking for duplication, and finding a gap instead

Before treating iam-lint as "done," I wanted to sanity check it against Prowler, the most widely used open-source cloud security tool, and honestly the one I'd reach for if I were doing this for real. If Prowler already caught everything iam-lint catches, that's useful to know. Arguably it's even the right outcome. There's no point maintaining a worse version of something that already exists.

So I went through Prowler's IAM checks, all 48 of them, sitting in prowler/providers/aws/services/iam/.

Two of my rules turned out to already be well covered, and covered better than my own version:

FULL_ADMIN_ACCESS overlaps with Prowler's iam_*_no_administrative_privileges family (separate checks for attached, inline, and customer-managed policies). PRIVILEGE_ESCALATION_RISK and PASSROLE_UNRESTRICTED overlap with iam_policy_allows_privilege_escalation, which implements all roughly 21 documented AWS privilege escalation methods from Rhino Security Labs' research. My four-action check is a reasonable simplification. Theirs is the comprehensive version, and I'd rather point people to that one.

But MISSING_MFA_CONDITION didn't overlap with anything. Prowler has several MFA-related IAM checks, things like iam_user_mfa_enabled_console_access, iam_administrator_access_with_mfa, and iam_root_mfa_enabled, and every single one of them checks whether a user or role has an MFA device registered. None of them check whether an IAM policy's Condition block actually requires aws:MultiFactorAuthPresent before allowing a sensitive action.

That distinction matters more than it sounds like it should. A user can have MFA enabled on their account in general, while a policy attached to them (or a role they're able to assume) still permits iam:CreateAccessKey or sts:AssumeRole with no condition enforcing MFA at the moment that specific API call actually happens. "Has MFA" and "this action requires MFA" are two different guarantees. AWS's own documentation treats aws:MultiFactorAuthPresent as a distinct control for exactly this reason.

Filing it properly, not just assuming

I didn't want to just assume this was a real gap and go straight to opening a PR. Prowler is a mature, actively maintained project, and "new check" proposals have knock-on effects for their compliance framework mappings that I'm honestly not close enough to the project to judge on my own. So instead I used their "New Check Request" issue template, which asks for exactly this kind of scoping: what should be detected, what PASS and FAIL mean, references, and confirmation that you've actually searched for duplicates first.

I ran that duplicate search three different ways against Prowler Hub, their check catalog search, before ticking that box: "policy level mfa," MultiFactorAuthPresent, and "condition mfa." All three came back empty.

The proposal went in as issue #12559, a new check called iam_policy_sensitive_actions_require_mfa_condition, severity medium (matching how I'd rated the equivalent rule in iam-lint, since it's a defense-in-depth gap rather than something independently exploitable the way a full-admin policy is). I linked back to iam-lint's has_mfa_condition() function as a working reference implementation, since I'd already built and tested the exact logic the check would need.

Where it stands

The issue is filed and sitting in maintainer triage as I write this. If it's accepted, the actual implementation work is mostly translation. Prowler has its own check class structure (a metadata.json file plus a Check subclass plus tests), but the core detection logic already exists and is already tested. And if it turns out there's a reason it doesn't fit, maybe a naming convention I'm not aware of, or an existing effort somewhere I missed, that's useful feedback too.

Either way, I think the process was worth writing up. Reading someone else's 48 checks closely enough to find the one real gap, instead of just assuming your own tool is automatically additive, is a different skill than writing the checks in the first place. And it's probably the more useful one if the actual goal is contributing to tools other people rely on, rather than just building your own in isolation.

iam-lint is open source at github.com/Bims-creator/iam-lint if you want to see the full ruleset or try it against your own policies.

Top comments (0)