<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Cloud Security Engineering]]></title><description><![CDATA[Cloud security from the operating side: how detections get built, how much they really cover, and  how to find out what a scan could not see.]]></description><link>https://gustavoortega.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Cloud Security Engineering</title><link>https://gustavoortega.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 25 Sep 2026 18:06:14 GMT</lastBuildDate><atom:link href="https://gustavoortega.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Pick a rule, pick a resource, see what it catches]]></title><description><![CDATA[Most encryption rules only check value: false. Which of these three RDS instances does this one report?
filters:
- or:
  - type: value
    key: StorageEncrypted
    value: false
  - type: value
    ke]]></description><link>https://gustavoortega.hashnode.dev/pick-a-rule-pick-a-resource-see-what-it-catches</link><guid isPermaLink="true">https://gustavoortega.hashnode.dev/pick-a-rule-pick-a-resource-see-what-it-catches</guid><category><![CDATA[AWS]]></category><category><![CDATA[cloud security]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Gustavo Martin Ortega Marmenti]]></dc:creator><pubDate>Wed, 16 Sep 2026 13:02:04 GMT</pubDate><content:encoded><![CDATA[<p>Most encryption rules only check <code>value: false</code>. Which of these three RDS instances does this one report?</p>
<pre><code class="language-yaml">filters:
- or:
  - type: value
    key: StorageEncrypted
    value: false
  - type: value
    key: StorageEncrypted
    value: absent
</code></pre>
<pre><code class="language-json">{"DBInstanceIdentifier": "a", "StorageEncrypted": true}
{"DBInstanceIdentifier": "b", "StorageEncrypted": false}
{"DBInstanceIdentifier": "c"}
</code></pre>
<p>Instance <code>a</code> is encrypted, so no. Instance <code>b</code> is not, so yes.</p>
<p>Instance <code>c</code> has no <code>StorageEncrypted</code> at all. AWS never sent it. Most people expect the first condition to catch that. It does not.</p>
<p><img src="https://gustavoortega.github.io/cloud-custodian-compliance-policies/press/diagram.png" alt="How a missing key escapes the first condition and is caught by the second" /></p>
<p>In Python a missing key is <code>None</code>, and <code>None == False</code> is false. So the first condition skips instance <code>c</code>. The second one catches it.</p>
<p>Write the rule without that second condition and instance <code>c</code> is never reported. Nothing fails. You get one finding fewer, which looks like good news.</p>
<p>I counted in my own catalogue: <strong>89 policies compared a boolean to <code>false</code>, and 23 had no <code>absent</code> branch for that key.</strong> Mine, written by someone who has been staring at this exact bug for months.</p>
<p>I went through the 23 one by one. Nineteen were fine: the field is always in the response, or the check is deliberate and the file says why. Four were real. Two of those turned out to be worse than a missing branch.</p>
<p>The KMS rotation check read a value that is never set when the API call is denied, so a key nobody could read counted as rotating. And a pair of WAF policies had been matching nothing at all, for every account, since the day they were written: the field they filter on sits one level deeper than the filter looked. Zero findings for years, and zero findings is what a clean account looks like.</p>
<p>All five are fixed. Run <code>python ci/count_absent_guards.py</code> today and you get 84 and 17, and the 17 are the documented ones.</p>
<h2>A page that answers the quiz for you</h2>
<p><img src="https://gustavoortega.github.io/cloud-custodian-compliance-policies/press/shot-playground.png" alt="The rule, the resource it runs on, and the answer condition by condition" /></p>
<p>Pick a rule, pick a resource, see what each condition answered. This is instance <code>c</code>: one condition says no match, the other is the reason it shows up at all. <a href="https://gustavoortega.github.io/cloud-custodian-compliance-policies/">Open it</a>.</p>
<p>A test tells you whether a policy matched. This tells you which condition did it.</p>
<p>With two conditions you can work it out in your head. With an <code>or</code> nested inside an <code>and</code>, over a resource with forty fields, you cannot, and the usual answer is to run it against a real account and see.</p>
<h2>Building a rule, and seeing what it catches</h2>
<p>The second screen writes the YAML for you. Pick a resource type, a field, a comparison.</p>
<p><img src="https://gustavoortega.github.io/cloud-custodian-compliance-policies/press/shot-builder.png" alt="The builder: a form, the YAML it writes, and three resources showing what the rule catches" /></p>
<p>The panel at the bottom is the point. Three resources, and what your rule does to each one, while you type.</p>
<p>Untick "also match when the key never came back" and the third resource goes from <strong>fires</strong> to <strong>no match</strong>. That is the bug at the top of this post, in one checkbox, while you are writing the rule.</p>
<p>The fields it offers are the ones these policies already use on that resource type.</p>
<h2>Your own resource</h2>
<p>Copy a resource out of a run that surprised you, paste it, and you get the same answer for your data. It stays in your browser.</p>
<p>That replaces the slow loop: edit the YAML, run <code>custodian run --dryrun</code> against an account, read the output file.</p>
<p>You paste a resource, not a policy. The rules come from the catalogue, and the browser evaluator covers part of c7n's value filter: anything outside it comes back <em>unknown</em>, never a verdict.</p>
<h2>Try it</h2>
<ul>
<li><a href="https://gustavoortega.github.io/cloud-custodian-compliance-policies/">The site</a>: 325 AWS policies, each mapped to its FSBP, PCI DSS 4.0, CIS or SOX control, and 451 example resources taken from the tests that ship with them.</li>
<li><a href="https://github.com/gustavoortega/cloud-custodian-compliance-policies">The policies</a>, Apache 2.0.</li>
<li><a href="https://github.com/gustavoortega/c7n-kit">c7n-kit</a>, which computes the answers: <code>pip install c7n-kit</code>.</li>
</ul>
<p>If one of your policies behaves differently from what the page says, send me the resource. That is the interesting case.</p>
]]></content:encoded></item><item><title><![CDATA[Cloud Custodian ships an engine and no rules. Here are 325.]]></title><description><![CDATA[Cloud Custodian is an engine with no rules in it.
You install it, write a policy, it works, you like it. You write another one. Months later there are thirty YAML files that different people wrote on ]]></description><link>https://gustavoortega.hashnode.dev/cloud-custodian-ships-an-engine-and-no-rules-here-are-325</link><guid isPermaLink="true">https://gustavoortega.hashnode.dev/cloud-custodian-ships-an-engine-and-no-rules-here-are-325</guid><category><![CDATA[AWS]]></category><category><![CDATA[cloudcustodian]]></category><category><![CDATA[cloud security]]></category><category><![CDATA[compliance ]]></category><category><![CDATA[DevSecOps]]></category><category><![CDATA[Security]]></category><category><![CDATA[Python]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[policy as code]]></category><dc:creator><![CDATA[Gustavo Martin Ortega Marmenti]]></dc:creator><pubDate>Mon, 03 Aug 2026 19:19:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a6e9105fa3700168a9f47b2/cfba0501-8372-49cd-a51b-f2d98ffe4d84.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Cloud Custodian is an engine with no rules in it.</p>
<p>You install it, write a policy, it works, you like it. You write another one. Months later there are thirty YAML files that different people wrote on different days, someone asks what your PCI coverage is, and you have no answer. Not a bad one. None.</p>
<p>The official docs have 109 example policies and not one of them says which control it covers. Prowler ships that mapping for free, so does Steampipe. Cloud Custodian never did.</p>
<p>So here is mine: <strong>325 policies, one YAML file per AWS service, each naming the control it covers.</strong></p>
<p><a href="https://github.com/gustavoortega/cloud-custodian-compliance-policies">github.com/gustavoortega/cloud-custodian-compliance-policies</a></p>
<h2>Using them</h2>
<p>Take the file for a service and run it. Nothing to adopt, no wrapper.</p>
<pre><code class="language-plaintext">$ custodian run -s out policies/aws/rds.yml
</code></pre>
<p>That is 34 RDS policies against whatever your credentials point at. Or copy five into the YAML you already have. They are ordinary c7n policies.</p>
<p>For a whole organisation the layout already matches what <code>c7n-org</code> expects:</p>
<pre><code class="language-plaintext">$ c7n-org run -c accounts.yml -s out -u policies/aws/
</code></pre>
<p>One thing before you do. Some policies carry an allow-list of account IDs that ships as <code>111111111111</code> with a <code>REPLACE</code> comment. Those filters treat anything off the list as external, so until you put your own accounts in they will flag every grant between your own accounts. Loud, never quiet.</p>
<h2>What the mapping buys you</h2>
<p>Each policy carries this:</p>
<pre><code class="language-yaml">  metadata:
    severity: high
    frameworks: ['FSBP RDS.3', 'PCI 3.5.1']
</code></pre>
<p>Plain c7n, no fork, and it travels into the run's output. Every finding arrives knowing its control, so your dashboard groups by framework without a lookup table you maintain by hand.</p>
<p>And you can finally answer the question:</p>
<pre><code class="language-plaintext">$ c7n-kit coverage catalogs/fsbp.txt policies/aws

FSBP  229/369 controls (62%)
orphans: none
</code></pre>
<p>229 of the 369 controls in AWS Foundational Security Best Practices. That one gets a percentage because I rebuilt the catalogue from the AWS docs page by page and know it is complete. PCI, CIS and SOX are mapped too, but their catalogues are partial, so those stay as counts. A number whose denominator you cannot defend is worse than no number.</p>
<p>Orphans are the other half: a policy claiming <code>FSBP RDS.99</code>, which does not exist, raises your coverage and errors on nothing. That check caught two in my own files.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a6e9105fa3700168a9f47b2/f522b5ac-b471-4aab-a5e6-ba0f0f87a9dc.png" alt="FSBP coverage by AWS service" style="display:block;margin:0 auto" />

<p>The repo has a page per framework with every control listed, covered or not, so "what is missing" is a link rather than an exercise.</p>
<h2>What it found</h2>
<p>I wrote the harness, then pointed it at the 325 policies I had just published.</p>
<p>74 of them were not doing what their YAML said.</p>
<p>The common one, forty-six: a <code>value: false</code> filter on a field AWS did not return. In c7n a missing key is <code>None</code>, and <code>None == False</code> is <code>False</code>, so the resource does not match and gets reported as compliant. If the field never came back you checked nothing, and the output looks identical to a pass.</p>
<p>Sixteen were worse. An absent key reaches a JMESPath function that cannot take one, the exception kills the run for that whole account, and the account writes no results. Which on a dashboard looks exactly like an account with nothing wrong.</p>
<p>Nine were missing a presence guard before the real filter. Five of those hid findings. The other four did the opposite and are worth knowing about, because they break the pattern: <code>op: not-equal</code> on an absent key returns <code>True</code>, not <code>False</code>. So those four reported healthy resources as findings. Same root cause, opposite symptom.</p>
<p>Three had never worked at all. One filtered on <code>Encrypted</code> when <code>DescribeDBClusterSnapshots</code> returns <code>StorageEncrypted</code>, so it matched zero and called every Aurora snapshot encrypted. One filtered on a key nothing populates and matched every table. One read a load balancer attribute that <code>DescribeLoadBalancers</code> does not return, so it flagged every classic load balancer on every run.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a6e9105fa3700168a9f47b2/4af6dccd-1f20-49fd-8c5f-68a71b4a1cde.png" alt="The 74" style="display:block;margin:0 auto" />

<p>None of it was visible from the output. They had been producing reasonable-looking findings for months.</p>
<p>All 74 are fixed, and every fix ships with the mutation that proves it: remove the guard and the test goes red. Eight more were deliberately left alone and say so in the file, marked <code>DELIBERATE: no absent branch</code> with the reason, because absence there means the safe state and widening the filter would have invented findings.</p>
<h2>Stricter than an internal catalogue, on purpose</h2>
<p>Somebody running this against their own accounts pointed out that in their fleet most of those fields come back 100% of the time, so the bug never fires and the fix adds nothing. They measured it, and they were right about their fleet.</p>
<p>A published catalogue cannot reason that way. Whoever clones this has a fleet nobody has measured, with services and tiers and regions I have never seen. A policy that reports an unverified resource as compliant is a defect for them even when it would never fire here. So the rule in this repo is the strict one: if the field did not come back, say so.</p>
<p>The same logic sets the limit. Two of the absent branches are scoped rather than open, because an open one would have been worse than the bug it fixed: MSK only for provisioned clusters, since serverless has no such block and mandatory TLS, and ACM renewal only for issued certificates, since the expiry date is absent by design before validation.</p>
<h2>What this is not</h2>
<p>It only detects. No policy here has an <code>actions:</code> block, because adding remediation to a rule you have not watched for a few weeks is how you find out what it really matches.</p>
<p>It does not replace Security Hub. AWS evaluates all 369 controls natively. I run these because a Security Hub control is a black box you can only suppress while a c7n policy is a filter you can read and fix, and because Config recording has a bill that not every account agreed to pay.</p>
<p>And it will age. Every AWS field rename rots a policy, and a rotted policy returns zero resources, which renders as compliant. Same failure as everything above, which is why the tests exist.</p>
<h2>One rule</h2>
<p>An absence is not a zero. A field that never came back is not a <code>false</code>, an account nobody could scan is not a clean account, a control ID that does not exist is not covered. Say unknown and leave it on the screen, because all of those errors point the same way, toward everything looking fine.</p>
<hr />
<p><a href="https://github.com/gustavoortega/cloud-custodian-compliance-policies">The policies</a> and <a href="https://github.com/gustavoortega/c7n-kit">the toolkit</a> are Apache 2.0. Issues and PRs welcome, especially for controls I have not covered.</p>
<p>The toolkit is on PyPI now, so you can run the tests without cloning it: <code>pip install c7n-kit</code>.</p>
]]></content:encoded></item></channel></rss>