<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bims-creator</title>
    <description>The latest articles on DEV Community by Bims-creator (@bimscreator).</description>
    <link>https://dev.to/bimscreator</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096407%2Fc8587141-7dc1-4606-86ca-4fb18b2b0019.png</url>
      <title>DEV Community: Bims-creator</title>
      <link>https://dev.to/bimscreator</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bimscreator"/>
    <language>en</language>
    <item>
      <title>I audited Prowler's 48 IAM checks against a linter I built, and found a real gap</title>
      <dc:creator>Bims-creator</dc:creator>
      <pubDate>Wed, 26 Aug 2026 23:52:27 +0000</pubDate>
      <link>https://dev.to/bimscreator/i-audited-prowlers-48-iam-checks-against-a-linter-i-built-and-found-a-real-gap-1h0c</link>
      <guid>https://dev.to/bimscreator/i-audited-prowlers-48-iam-checks-against-a-linter-i-built-and-found-a-real-gap-1h0c</guid>
      <description>&lt;p&gt;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 &lt;code&gt;iam:PassRole&lt;/code&gt;. It was meant to be throwaway interview prep.&lt;/p&gt;

&lt;p&gt;It didn't stay throwaway. I kept adding rules, and it turned into &lt;code&gt;iam-lint&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What iam-lint checks
&lt;/h2&gt;

&lt;p&gt;Seven rules, each backed by tests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Detects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FULL_ADMIN_ACCESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;td&gt;Wildcard &lt;code&gt;Action&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;Resource&lt;/code&gt; together in one statement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PRIVILEGE_ESCALATION_RISK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;iam:CreateAccessKey&lt;/code&gt;, &lt;code&gt;iam:AttachUserPolicy&lt;/code&gt;, &lt;code&gt;iam:PutUserPolicy&lt;/code&gt;, or &lt;code&gt;sts:AssumeRole&lt;/code&gt; granted without a resource restriction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WILDCARD_PRINCIPAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;td&gt;Trust/resource-based policy &lt;code&gt;Principal&lt;/code&gt; is &lt;code&gt;"*"&lt;/code&gt;, granting access to anyone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WILDCARD_ACTION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Action&lt;/code&gt; includes &lt;code&gt;"*"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WILDCARD_RESOURCE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Resource&lt;/code&gt; includes &lt;code&gt;"*"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PASSROLE_UNRESTRICTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;iam:PassRole&lt;/code&gt; not scoped to a specific role ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MISSING_MFA_CONDITION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;Sensitive actions allowed without requiring &lt;code&gt;aws:MultiFactorAuthPresent&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run it against a policy, and you get something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;iam-lint scan policy.json
&lt;span class="o"&gt;[&lt;/span&gt;HIGH] &lt;span class="o"&gt;[&lt;/span&gt;WILDCARD_ACTION] statement 0: Action includes &lt;span class="s2"&gt;"*"&lt;/span&gt;, granting every action.
&lt;span class="o"&gt;[&lt;/span&gt;HIGH] &lt;span class="o"&gt;[&lt;/span&gt;WILDCARD_RESOURCE] statement 0: Resource includes &lt;span class="s2"&gt;"*"&lt;/span&gt;, applying account-wide.
&lt;span class="o"&gt;[&lt;/span&gt;CRITICAL] &lt;span class="o"&gt;[&lt;/span&gt;FULL_ADMIN_ACCESS] statement 0: Action and Resource are both &lt;span class="s2"&gt;"*"&lt;/span&gt;, granting unrestricted admin access.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;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 (&lt;code&gt;Action&lt;/code&gt;, &lt;code&gt;Resource&lt;/code&gt;, and &lt;code&gt;Principal&lt;/code&gt; all do this), then check it against a wildcard or a small set of risky values. The one that took the most thought was &lt;code&gt;MISSING_MFA_CONDITION&lt;/code&gt;, because you're not looking at &lt;code&gt;Action&lt;/code&gt; or &lt;code&gt;Resource&lt;/code&gt; at all. You're reaching into the statement's &lt;code&gt;Condition&lt;/code&gt; block instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_mfa_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;condition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Condition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;operator_block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operator_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws:MultiFactorAuthPresent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;operator_block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That little function is what led to the interesting part of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking for duplication, and finding a gap instead
&lt;/h2&gt;

&lt;p&gt;Before treating &lt;code&gt;iam-lint&lt;/code&gt; 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 &lt;code&gt;iam-lint&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;So I went through Prowler's IAM checks, all 48 of them, sitting in &lt;code&gt;prowler/providers/aws/services/iam/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two of my rules turned out to already be well covered, and covered better than my own version:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FULL_ADMIN_ACCESS&lt;/code&gt; overlaps with Prowler's &lt;code&gt;iam_*_no_administrative_privileges&lt;/code&gt; family (separate checks for attached, inline, and customer-managed policies). &lt;code&gt;PRIVILEGE_ESCALATION_RISK&lt;/code&gt; and &lt;code&gt;PASSROLE_UNRESTRICTED&lt;/code&gt; overlap with &lt;code&gt;iam_policy_allows_privilege_escalation&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;MISSING_MFA_CONDITION&lt;/code&gt; didn't overlap with anything. Prowler has several MFA-related IAM checks, things like &lt;code&gt;iam_user_mfa_enabled_console_access&lt;/code&gt;, &lt;code&gt;iam_administrator_access_with_mfa&lt;/code&gt;, and &lt;code&gt;iam_root_mfa_enabled&lt;/code&gt;, 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 &lt;code&gt;Condition&lt;/code&gt; block actually requires &lt;code&gt;aws:MultiFactorAuthPresent&lt;/code&gt; before allowing a sensitive action.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;iam:CreateAccessKey&lt;/code&gt; or &lt;code&gt;sts:AssumeRole&lt;/code&gt; 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 &lt;code&gt;aws:MultiFactorAuthPresent&lt;/code&gt; as a distinct control for exactly this reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filing it properly, not just assuming
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;The proposal went in as &lt;a href="https://github.com/prowler-cloud/prowler/issues/12559" rel="noopener noreferrer"&gt;issue #12559&lt;/a&gt;, a new check called &lt;code&gt;iam_policy_sensitive_actions_require_mfa_condition&lt;/code&gt;, severity &lt;code&gt;medium&lt;/code&gt; (matching how I'd rated the equivalent rule in &lt;code&gt;iam-lint&lt;/code&gt;, since it's a defense-in-depth gap rather than something independently exploitable the way a full-admin policy is). I linked back to &lt;code&gt;iam-lint&lt;/code&gt;'s &lt;code&gt;has_mfa_condition()&lt;/code&gt; function as a working reference implementation, since I'd already built and tested the exact logic the check would need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;metadata.json&lt;/code&gt; file plus a &lt;code&gt;Check&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;iam-lint&lt;/code&gt; is open source at &lt;a href="https://github.com/Bims-creator/iam-lint" rel="noopener noreferrer"&gt;github.com/Bims-creator/iam-lint&lt;/a&gt; if you want to see the full ruleset or try it against your own policies.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>opensource</category>
      <category>python</category>
    </item>
  </channel>
</rss>
