<?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: Bala Paranj</title>
    <description>The latest articles on DEV Community by Bala Paranj (@bala_paranj_059d338e44e7e).</description>
    <link>https://dev.to/bala_paranj_059d338e44e7e</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%2F3862804%2F7ea6c560-63cb-4daf-a713-450532280b0a.jpg</url>
      <title>DEV Community: Bala Paranj</title>
      <link>https://dev.to/bala_paranj_059d338e44e7e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bala_paranj_059d338e44e7e"/>
    <language>en</language>
    <item>
      <title>One Security Question, Five Reasoning Engines, Zero Source Code</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Sat, 18 Jul 2026 11:50:21 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/one-security-question-five-reasoning-engines-zero-source-code-2i14</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/one-security-question-five-reasoning-engines-zero-source-code-2i14</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We ran an experiment. We took a security question — "can an unauthenticated principal reach a sensitive resource?" and wrote specifications for five different reasoning engines: Z3 (SMT solver), Soufflé (Datalog), Clingo (Answer Set Programming), SWI-Prolog, and PRISM (probabilistic model checking).&lt;/p&gt;

&lt;p&gt;Then we gave each spec to an AI agent with no access to our source code. Just the spec YAML, the exported facts from a configuration snapshot, and the export schema. No Go source, internal documentation or prior context about the tool.&lt;/p&gt;

&lt;p&gt;The agent produced correct security reasoning in all five paradigms. That result is about architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec as the contract
&lt;/h2&gt;

&lt;p&gt;Each reasoning spec is a YAML file with a fixed structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trial&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;souffle&lt;/span&gt;
  &lt;span class="na"&gt;question&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;How many assets are anonymously reachable via&lt;/span&gt;
    &lt;span class="s"&gt;self-registration or privilege-escalation paths?&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;input.facts&lt;/span&gt;
  &lt;span class="na"&gt;export_schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sir-predicates.md&lt;/span&gt;
  &lt;span class="na"&gt;reasoning_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Load the JSONL facts into Soufflé relations&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Apply the reachability rules from reachability.dl&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Count tuples in the anonymous_reachable relation&lt;/span&gt;
  &lt;span class="na"&gt;expected_output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;anonymous_reachable_count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;
  &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exact_match&lt;/span&gt;
    &lt;span class="na"&gt;ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;output_ordering&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whitespace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spec is simultaneously three things: a specification (what the engine should compute), an implementation guide (how to compute it), and a test definition (the correct answer). The golden output in &lt;code&gt;expected_output&lt;/code&gt; is committed alongside the spec and diffed against the agent's output.&lt;/p&gt;

&lt;p&gt;This means contributing a new security analysis doesn't require reading our Go codebase. A contributor writes a spec, commits the golden output, and the trial framework validates it. The spec is the interface. The source code is an implementation detail behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the trials found
&lt;/h2&gt;

&lt;p&gt;Five trials. Three passed on first run. Two failed. The failures were more informative than the passes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Z3 (SMT):&lt;/strong&gt; pass. The agent translated the JSONL facts into SMT-LIB assertions, ran &lt;code&gt;(check-sat)&lt;/code&gt;, and produced the correct verdict and witness. The question is a satisfiability problem. Can an unauthenticated principal read an S3 bucket with Public Access Block disabled. Z3 either finds a satisfying assignment (the specific principal and bucket) or proves none exists. The agent found the right one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Soufflé (Datalog):&lt;/strong&gt; pass. The agent loaded facts into relations, applied reachability rules, and counted 12 anonymously reachable assets. Byte-identical to the golden output. Datalog's strength here is transitive closure — role assumption chains of arbitrary depth are computed by fixed-point evaluation, not by bounded loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clingo (ASP):&lt;/strong&gt; fail, then pass. The agent's output didn't match the golden because the spec used &lt;code&gt;mfa_enforced&lt;/code&gt; but the exported facts used &lt;code&gt;has_mfa_enforced&lt;/code&gt;. This was a spec bug. A naming-convention slip where the spec author forgot the &lt;code&gt;has_&lt;/code&gt; prefix that Stave's fact catalog uses for all boolean predicates.&lt;/p&gt;

&lt;p&gt;The fix was two lines: correct the predicate name in the spec and add an explicit note about the naming convention. When we re-ran with a blind agent (fresh context, no knowledge of the fix), the agent picked up the convention note and produced correct output.&lt;/p&gt;

&lt;p&gt;This is the kind of defect a trial framework should catch. The spec was wrong. The engine was right. The framework distinguished between the two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prolog:&lt;/strong&gt; fail, then pass. The agent's output was correct. 12 proof chains, but the golden said 6. The spec author had transcribed the golden by hand and missed the DynamoDB action variants that create a cartesian product with the Cognito identity pool paths. The trial framework caught this by comparing the agent's output against the engine's actual output (12) rather than trusting the golden.&lt;/p&gt;

&lt;p&gt;Category A failure (Clingo): spec bug — agent couldn't derive engine output from spec. Category C failure (Prolog): golden bug — agent's output matched engine, but golden was wrong. The distinction is foundational. In Category A, you fix the spec. In Category C, you fix the golden. Conflating the two means you can't trust your test suite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PRISM (probabilistic):&lt;/strong&gt; pass. The agent computed per-attack-shape exploitation probabilities from the PRISM model. Step probabilities were exact. Aggregate probability was within ±0.005 of the golden. PRISM is the most unusual engine in the set. It doesn't prove safety, it quantifies risk. "Given this configuration, the probability of successful exploitation via the Cognito self-registration path is 0.342." That's a different kind of output than SAT/UNSAT, and the spec format handles it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blind re-run
&lt;/h2&gt;

&lt;p&gt;The original trials had a methodological weakness: the spec author and the trial runner were in the same session. The agent might have been influenced by context from the authoring process.&lt;/p&gt;

&lt;p&gt;So we re-ran the two trials that had spec changes (Clingo and Prolog) with a fresh sub-agent. No prior context, knowledge of the original failures or fixes. Just the corrected spec YAML, the input facts, and the export schema.&lt;/p&gt;

&lt;p&gt;Both passed.&lt;/p&gt;

&lt;p&gt;The Prolog blind agent derived 12 proof chains via the cartesian product, matching the post-fix golden count—all without seeing the explanation comment in the spec.&lt;/p&gt;

&lt;p&gt;It independently discovered the same combinatorial structure the original agent had found.&lt;/p&gt;

&lt;p&gt;The Clingo blind agent correctly used &lt;code&gt;has_mfa_enforced&lt;/code&gt; with the &lt;code&gt;has_&lt;/code&gt; prefix. The naming-convention note in the spec was sufficient for a context-free agent to recover the convention.&lt;/p&gt;

&lt;p&gt;The blind run also surfaced two non-issues: output ordering and indentation width. Both were already in the spec's &lt;code&gt;ignore:&lt;/code&gt; list — confirming that the ignore directives are doing their job of preventing false failures on cosmetically different but semantically correct output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five paradigms, one contract
&lt;/h2&gt;

&lt;p&gt;Each engine answers a different kind of question about the same configuration snapshot:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Paradigm&lt;/th&gt;
&lt;th&gt;Question shape&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Z3&lt;/td&gt;
&lt;td&gt;SMT&lt;/td&gt;
&lt;td&gt;"Is it possible for X?"&lt;/td&gt;
&lt;td&gt;SAT/UNSAT + witness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soufflé&lt;/td&gt;
&lt;td&gt;Datalog&lt;/td&gt;
&lt;td&gt;"What can reach what?"&lt;/td&gt;
&lt;td&gt;All reachable tuples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clingo&lt;/td&gt;
&lt;td&gt;ASP&lt;/td&gt;
&lt;td&gt;"Which assets violate which rules?"&lt;/td&gt;
&lt;td&gt;Violation atoms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prolog&lt;/td&gt;
&lt;td&gt;Logic&lt;/td&gt;
&lt;td&gt;"Prove the chain from A to B"&lt;/td&gt;
&lt;td&gt;Proof trees&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRISM&lt;/td&gt;
&lt;td&gt;Probabilistic&lt;/td&gt;
&lt;td&gt;"How likely is exploitation?"&lt;/td&gt;
&lt;td&gt;Risk probabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The input to all five is the same: Stave's JSONL fact export from a single configuration snapshot. The export contract — &lt;code&gt;has_action(arn, action)&lt;/code&gt;, &lt;code&gt;trusts_service(arn, service)&lt;/code&gt;, &lt;code&gt;has_tag(arn, key, value)&lt;/code&gt; — is the stable interface. Each engine consumes the same facts in its native format.&lt;/p&gt;

&lt;p&gt;This is the architectural boundary the trials validated: Stave's export contracts are complete for five reasoning paradigms. No engine reported a missing input field. Every engine produced correct output from the exported facts alone. The contract layer is tested, not asserted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for contributors
&lt;/h2&gt;

&lt;p&gt;The reasoning-specs directory is open. The steps for adding a new security analysis:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick a security question and a reasoning engine.&lt;/li&gt;
&lt;li&gt;Write a spec YAML: the question, the reasoning steps, the expected output.&lt;/li&gt;
&lt;li&gt;Commit the golden output.&lt;/li&gt;
&lt;li&gt;Run the trial. If it passes, the analysis is validated. If it fails, the framework tells you whether the bug is in the spec, the golden, or the engine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No Go source code required. The spec references the export schema (which predicates are available, what they mean) and the input facts (a JSONL file from any Stave snapshot). A contributor who knows Z3 but not Go can write an SMT-based analysis. A contributor who knows Prolog but not cloud security can implement a proof-chain analysis from a well-written spec.&lt;/p&gt;

&lt;p&gt;Several engines don't have trial packages yet; PySAT, TLA+, game theory, STIX, JSON-LD/GraphML, OSCAL/OCSF, and compliance evidence packs are all open for contribution. The spec format is documented. The trial framework is committed. The export facts are stable.&lt;/p&gt;

&lt;p&gt;The prerequisite is a working example with a committed, fixture-tied golden output. The spec wraps the example. The trial validates the spec. The blind re-run validates the trial. Each layer adds confidence without requiring access to the layer below it.&lt;/p&gt;




&lt;p&gt;Stave is an open-source cloud security reasoning engine. The reasoning-specs trials, input fixtures, and export schemas are at &lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;stave&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
      <category>cloudsecurity</category>
    </item>
    <item>
      <title>Rhino Found 21. Z3 Found 50.</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Fri, 17 Jul 2026 11:11:47 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/rhino-found-21-z3-found-50-4bfg</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/rhino-found-21-z3-found-50-4bfg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In 2018 Spencer Gietzen at Rhino Security Labs published the definitive reference on AWS IAM privilege escalation: 21 named methods, each a specific action or action combination that turns limited permissions into administrative ones. The research is cited by every AWS security tool. PMapper checks for it. Pacu exploits it. Prowler enumerates it. Stave has 44+ per-technique controls, one per method or close enough.&lt;/p&gt;

&lt;p&gt;The 21 methods are correctly identified. The math says there are at least 50.&lt;/p&gt;

&lt;p&gt;This article runs Rhino's research through a Z3 SAT solver against a synthetic principal with a permission set Rhino's research was written to characterize. The solver finds all 21 of Rhino's methods &lt;em&gt;and&lt;/em&gt; 27 additional methods Rhino didn't enumerate. The additional methods are the same structural shapes — "can the principal modify its own permissions?", "can the principal launch compute with a privileged role?" — applied to AWS services that exist today but weren't on Rhino's enumeration in 2018.&lt;/p&gt;

&lt;p&gt;The killer follow-up: write a Deny policy that blocks every action on Rhino's list. Re-run the solver. &lt;strong&gt;24 methods still reachable.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The collapse
&lt;/h2&gt;

&lt;p&gt;Rhino enumerated 21 methods. They group into 5 structural patterns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Rhino methods&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Policy Self-Mutation&lt;/td&gt;
&lt;td&gt;1, 2, 7-13 (9 methods)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Credential Creation / Theft&lt;/td&gt;
&lt;td&gt;4, 5, 6, 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Compute + PassRole&lt;/td&gt;
&lt;td&gt;3, 15-21 (8 methods)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Indirect Compute Invocation&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Role Trust Modification&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each pattern is one structural shape: "principal can modify own permissions," "principal can hijack another principal's credentials," "principal can launch compute with a role," "principal can trigger compute indirectly," "principal can change a role's trust." The 21 methods are 21 &lt;em&gt;instances&lt;/em&gt; of these five shapes.&lt;/p&gt;

&lt;p&gt;A checklist scanner writes a separate predicate for each instance: 21 checks. A Z3 prover writes a separate query for each &lt;em&gt;shape&lt;/em&gt;: 5 queries. The collapse ratio is 21 → 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Z3 prover
&lt;/h2&gt;

&lt;p&gt;For each pattern, the prover holds a registry of known methods that fit the shape. Pattern 3's registry, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;ec2&lt;/span&gt;:&lt;span class="n"&gt;RunInstances&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                                    [&lt;span class="n"&gt;Rhino&lt;/span&gt;  &lt;span class="m"&gt;3&lt;/span&gt;]
&lt;span class="n"&gt;lambda&lt;/span&gt;:&lt;span class="n"&gt;CreateFunction&lt;/span&gt; + &lt;span class="n"&gt;lambda&lt;/span&gt;:&lt;span class="n"&gt;InvokeFunction&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;       [&lt;span class="n"&gt;Rhino&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;]
&lt;span class="n"&gt;lambda&lt;/span&gt;:&lt;span class="n"&gt;CreateFunction&lt;/span&gt; + &lt;span class="n"&gt;lambda&lt;/span&gt;:&lt;span class="n"&gt;CreateEventSourceMapping&lt;/span&gt;            [&lt;span class="n"&gt;Rhino&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;]
&lt;span class="n"&gt;lambda&lt;/span&gt;:&lt;span class="n"&gt;UpdateFunctionCode&lt;/span&gt;                                          [&lt;span class="n"&gt;Rhino&lt;/span&gt; &lt;span class="m"&gt;17&lt;/span&gt;]
&lt;span class="n"&gt;glue&lt;/span&gt;:&lt;span class="n"&gt;CreateDevEndpoint&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                              [&lt;span class="n"&gt;Rhino&lt;/span&gt; &lt;span class="m"&gt;18&lt;/span&gt;]
&lt;span class="n"&gt;glue&lt;/span&gt;:&lt;span class="n"&gt;UpdateDevEndpoint&lt;/span&gt;                                             [&lt;span class="n"&gt;Rhino&lt;/span&gt; &lt;span class="m"&gt;19&lt;/span&gt;]
&lt;span class="n"&gt;cloudformation&lt;/span&gt;:&lt;span class="n"&gt;CreateStack&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                          [&lt;span class="n"&gt;Rhino&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;]
&lt;span class="n"&gt;datapipeline&lt;/span&gt;:&lt;span class="n"&gt;CreatePipeline&lt;/span&gt; + &lt;span class="n"&gt;datapipeline&lt;/span&gt;:&lt;span class="n"&gt;PutPipelineDefinition&lt;/span&gt;   [&lt;span class="n"&gt;Rhino&lt;/span&gt; &lt;span class="m"&gt;21&lt;/span&gt;]
&lt;span class="n"&gt;autoscaling&lt;/span&gt;:&lt;span class="n"&gt;CreateLaunchConfiguration&lt;/span&gt; + &lt;span class="n"&gt;CreateAutoScalingGroup&lt;/span&gt;     [&lt;span class="n"&gt;NEW&lt;/span&gt;]
&lt;span class="n"&gt;ecs&lt;/span&gt;:&lt;span class="n"&gt;RunTask&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                                         [&lt;span class="n"&gt;NEW&lt;/span&gt;]
&lt;span class="n"&gt;ecs&lt;/span&gt;:&lt;span class="n"&gt;CreateService&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                                   [&lt;span class="n"&gt;NEW&lt;/span&gt;]
&lt;span class="n"&gt;codebuild&lt;/span&gt;:&lt;span class="n"&gt;CreateProject&lt;/span&gt; + &lt;span class="n"&gt;codebuild&lt;/span&gt;:&lt;span class="n"&gt;StartBuild&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;      [&lt;span class="n"&gt;NEW&lt;/span&gt;]
&lt;span class="n"&gt;sagemaker&lt;/span&gt;:&lt;span class="n"&gt;CreateNotebookInstance&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                    [&lt;span class="n"&gt;NEW&lt;/span&gt;]
&lt;span class="n"&gt;sagemaker&lt;/span&gt;:&lt;span class="n"&gt;CreateTrainingJob&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                         [&lt;span class="n"&gt;NEW&lt;/span&gt;]
&lt;span class="n"&gt;batch&lt;/span&gt;:&lt;span class="n"&gt;SubmitJob&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                                     [&lt;span class="n"&gt;NEW&lt;/span&gt;]
&lt;span class="n"&gt;states&lt;/span&gt;:&lt;span class="n"&gt;CreateStateMachine&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                           [&lt;span class="n"&gt;NEW&lt;/span&gt;]
&lt;span class="n"&gt;apprunner&lt;/span&gt;:&lt;span class="n"&gt;CreateService&lt;/span&gt; + &lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PassRole&lt;/span&gt;                             [&lt;span class="n"&gt;NEW&lt;/span&gt;]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight Rhino methods. Nine additions. Same shape: any service that lets you launch compute with a specified IAM role is in the same structural class. Adding a new service to the registry is one struct entry. No new predicate, control YAML or check.&lt;/p&gt;

&lt;p&gt;The query: "is there a method in this registry whose actions are all effectively allowed for the principal (in some Allow statement, not in any Deny)?" The solver returns the index of any reachable method. SAT iff the registry has at least one reachable entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full results on a vulnerable principal
&lt;/h2&gt;

&lt;p&gt;Run the prover against a principal that grants every relevant action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;====================================================================
== rhino-vulnerable (all 21 Rhino methods enabled)
====================================================================

--- Pattern 1: Policy Self-Mutation ---
  registry size: 13 methods (9 Rhino-numbered + 4 additional)
  reachable:     13 / 13 methods
  verdict:       SAT — at least one method reachable; full list:
    [Rhino 01] iam:CreatePolicyVersion → activate admin version on a policy the principal uses
    [Rhino 02] iam:SetDefaultPolicyVersion → activate dormant admin version
    [Rhino 07] iam:AttachUserPolicy → attach AdministratorAccess to self
    [Rhino 08] iam:AttachGroupPolicy → attach admin to own group
    [Rhino 09] iam:AttachRolePolicy → attach admin to assumable role
    [Rhino 10] iam:PutUserPolicy → create admin inline on self
    [Rhino 11] iam:PutGroupPolicy → create admin inline on own group
    [Rhino 12] iam:PutRolePolicy → create admin inline on own role
    [Rhino 13] iam:AddUserToGroup → join admin group
    [NEW    ] iam:CreatePolicy + iam:AttachUserPolicy → create admin policy and attach
    [NEW    ] iam:DeleteUserPolicy + iam:PutUserPolicy → drop restrictive inline, replace
    [NEW    ] iam:DetachUserPolicy + iam:AttachUserPolicy → swap boundary for admin
    [NEW    ] iam:DeleteRolePermissionsBoundary → remove permissions boundary on assumable role

--- Pattern 2: Credential Creation / Theft ---
  registry size: 7 methods (4 Rhino-numbered + 3 additional)
  reachable:     7 / 7 methods
  verdict:       SAT
    [Rhino 04] iam:CreateAccessKey
    [Rhino 05] iam:CreateLoginProfile
    [Rhino 06] iam:UpdateLoginProfile
    [Rhino 14] iam:UpdateAssumeRolePolicy
    [NEW    ] iam:CreateVirtualMFADevice + iam:EnableMFADevice
    [NEW    ] iam:DeactivateMFADevice
    [NEW    ] sts:GetFederationToken

--- Pattern 3: Compute + PassRole ---
  registry size: 17 methods (8 Rhino-numbered + 9 additional)
  reachable:     17 / 17 methods
  verdict:       SAT
    (all 17 methods listed)

--- Pattern 4: Indirect Compute Invocation ---
  registry size: 10 methods (1 Rhino-numbered + 9 additional)
  reachable:     10 / 10 methods
  verdict:       SAT
    (all 10 methods listed)

--- Pattern 5: Role Trust Modification ---
  registry size: 3 methods (1 Rhino-numbered + 2 additional)
  reachable:     3 / 3 methods
  verdict:       SAT
    (all 3 methods listed)

--- Cross-pattern summary ---
  registry total: 50 methods across 5 patterns
  reachable:      50 methods
  Rhino's 21 hit: 21 / 21
  beyond Rhino:   27 methods (cross-pattern, with overlaps)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;50 reachable methods. 5 queries. Every one of Rhino's 21 confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Script Said "Confirmed." The Attack Failed.
&lt;/h2&gt;

&lt;p&gt;The collapse ratio above understates the value. "Rhino found 21, Z3 found 50" sounds like Z3's contribution is &lt;em&gt;more methods&lt;/em&gt;. That framing misses the bigger win: Z3 also eliminates &lt;em&gt;false positives&lt;/em&gt; the script-style checkers report.&lt;/p&gt;

&lt;p&gt;A real-world penetration test, written up by Security Shenanigans (October 2020), illustrates the problem. The attacker had &lt;code&gt;iam:PassRole&lt;/code&gt; and &lt;code&gt;ec2:RunInstances&lt;/code&gt;. Pattern 3 / Rhino Method 3 looks for this exact pair. &lt;code&gt;aws_escalate.py&lt;/code&gt; ran and reported:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Confirmed — privilege escalation possible&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The attacker launched an EC2 instance with an admin role attached. The instance started. The reverse-shell user-data fired. &lt;strong&gt;Nothing connected back.&lt;/strong&gt; The default security group had no egress rules — the standard "least-privilege" default that AWS ships. The escalation was confirmed but not exploitable.&lt;/p&gt;

&lt;p&gt;The attacker eventually pivoted: enumerated security groups (&lt;code&gt;ec2:DescribeSecurityGroups&lt;/code&gt;), found a &lt;code&gt;hadoop-cluster&lt;/code&gt; SG with full egress, enumerated subnets (&lt;code&gt;ec2:DescribeSubnets&lt;/code&gt;) for one in the same VPC, and re-launched. The escalation worked. But it required four constraints the script never checked.&lt;/p&gt;

&lt;p&gt;This is shipped in the example as &lt;code&gt;fixtures/real-world-pattern3/&lt;/code&gt;. Five assets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;arn&lt;/span&gt;:&lt;span class="n"&gt;aws&lt;/span&gt;:&lt;span class="n"&gt;iam&lt;/span&gt;:::&lt;span class="n"&gt;user&lt;/span&gt;/&lt;span class="n"&gt;rhino&lt;/span&gt;-&lt;span class="n"&gt;attacker&lt;/span&gt;          (&lt;span class="n"&gt;PassRole&lt;/span&gt;, &lt;span class="n"&gt;RunInstances&lt;/span&gt;, &lt;span class="n"&gt;ListRoles&lt;/span&gt;, &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;GetRole&lt;/span&gt;)
&lt;span class="n"&gt;arn&lt;/span&gt;:&lt;span class="n"&gt;aws&lt;/span&gt;:&lt;span class="n"&gt;iam&lt;/span&gt;:::&lt;span class="n"&gt;role&lt;/span&gt;/&lt;span class="n"&gt;danger&lt;/span&gt;-&lt;span class="n"&gt;role&lt;/span&gt;             (&lt;span class="n"&gt;admin&lt;/span&gt;, &lt;span class="n"&gt;trusts&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;.&lt;span class="n"&gt;amazonaws&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;)
&lt;span class="n"&gt;arn&lt;/span&gt;:&lt;span class="n"&gt;aws&lt;/span&gt;:&lt;span class="n"&gt;ec2&lt;/span&gt;:::&lt;span class="n"&gt;security&lt;/span&gt;-&lt;span class="n"&gt;group&lt;/span&gt;/&lt;span class="n"&gt;sg&lt;/span&gt;-&lt;span class="n"&gt;f73b339e&lt;/span&gt;   (&lt;span class="n"&gt;default&lt;/span&gt;, &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;egress&lt;/span&gt;)
&lt;span class="n"&gt;arn&lt;/span&gt;:&lt;span class="n"&gt;aws&lt;/span&gt;:&lt;span class="n"&gt;ec2&lt;/span&gt;:::&lt;span class="n"&gt;security&lt;/span&gt;-&lt;span class="n"&gt;group&lt;/span&gt;/&lt;span class="n"&gt;sg&lt;/span&gt;-&lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="n"&gt;csce3f&lt;/span&gt;   (&lt;span class="n"&gt;hadoop&lt;/span&gt;-&lt;span class="n"&gt;cluster&lt;/span&gt;, &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="n"&gt;egress&lt;/span&gt;)
&lt;span class="n"&gt;arn&lt;/span&gt;:&lt;span class="n"&gt;aws&lt;/span&gt;:&lt;span class="n"&gt;ec2&lt;/span&gt;:::&lt;span class="n"&gt;subnet&lt;/span&gt;/&lt;span class="n"&gt;subnet&lt;/span&gt;-&lt;span class="n"&gt;a213as8c&lt;/span&gt;       (&lt;span class="n"&gt;vpc&lt;/span&gt;-&lt;span class="n"&gt;a1b2c3d4&lt;/span&gt;, &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="n"&gt;both&lt;/span&gt; &lt;span class="n"&gt;SGs&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Z3 encodes the full conjunction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exploitable = passrole
            ∧ run_instances
            ∧ can_discover_role          ; ListRoles OR GetRole
            ∧ role_is_admin
            ∧ role_trusts_ec2
            ∧ exists_egress_sg           ; DescribeSGs ∧ SG.has_egress
            ∧ exists_valid_subnet        ; DescribeSubnets ∧ subnet.vpc=sg.vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three queries on the same fixture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- Default SG only (no egress) ---
  verdict: UNSAT
  failed:  security group sg-f73b339e has no egress rules — reverse-shell
           user-data cannot connect back

--- Hadoop SG (full egress) ---
  verdict: SAT — full compound satisfied
  witness: PassRole→arn:aws:iam:::role/danger-role + RunInstances + ListRoles
           + sg=sg-42csce3f + subnet in vpc=vpc-a1b2c3d4

--- Without iam:ListRoles (no role discovery) ---
  verdict: UNSAT
  failed:  role discovery (no iam:ListRoles, no iam:GetRole)
  note:    PassRole on Resource:* is useless without knowing which
           role to pass — script checkers miss this
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three different verdicts on three different scenarios that all share the PassRole + RunInstances pair the script keys on. The script reports "Confirmed" three times. Z3 reports SAT once.&lt;/p&gt;

&lt;h3&gt;
  
  
  The granular permission bypass
&lt;/h3&gt;

&lt;p&gt;Notice the third clause: &lt;code&gt;iam:ListRoles OR iam:GetRole&lt;/code&gt;. The real-world fixture allows &lt;code&gt;ListRoles&lt;/code&gt; but &lt;strong&gt;denies&lt;/strong&gt; &lt;code&gt;GetRole&lt;/code&gt;. A defender who reads "block GetRole" as the fix is wrong. &lt;code&gt;ListRoles&lt;/code&gt; returns the role's trust policy in the response payload. The attacker doesn't need &lt;code&gt;GetRole&lt;/code&gt; to read a trust policy; &lt;code&gt;ListRoles&lt;/code&gt; already discloses it. Z3's disjunction encodes this correctly. A script-style checker that lists "GetRole" as the discovery permission misses the bypass entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters for the headline number
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;aws_escalate.py&lt;/th&gt;
&lt;th&gt;Z3 compound&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PassRole + RunInstances&lt;/td&gt;
&lt;td&gt;Confirmed&lt;/td&gt;
&lt;td&gt;necessary, not sufficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discoverable admin role&lt;/td&gt;
&lt;td&gt;not checked&lt;/td&gt;
&lt;td&gt;ListRoles OR GetRole&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role trusts ec2.amazonaws.com&lt;/td&gt;
&lt;td&gt;not checked&lt;/td&gt;
&lt;td&gt;trust policy clause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security group with egress&lt;/td&gt;
&lt;td&gt;not checked&lt;/td&gt;
&lt;td&gt;sg.egress_rules ≠ ∅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subnet in SG's VPC&lt;/td&gt;
&lt;td&gt;not checked&lt;/td&gt;
&lt;td&gt;subnet.vpc_id = sg.vpc_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;"Confirmed"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SAT or UNSAT&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The script checks 1 condition. Z3 checks 5. In a real environment where default SGs have no egress, the script's "Confirmed" is wrong on every account whose attacker hasn't yet enumerated SGs and subnets. Z3 is right relative to the modeled constraints and when the model is incomplete (missing AMI availability, for example) it returns UNSAT instead of false-positive SAT.&lt;/p&gt;

&lt;p&gt;So the headline is wrong. "Rhino found 21, Z3 found 50" sounds like a counting argument. The sharper claim is two-sided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recall:&lt;/strong&gt; Z3 finds the methods Rhino enumerated &lt;em&gt;and&lt;/em&gt; finds methods Rhino didn't, in the same query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precision:&lt;/strong&gt; Z3 reports UNSAT when the compound that matters fails — &lt;em&gt;even if the action-list checker reports "Confirmed."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Bybit/Safe{WALLET} extension to the &lt;code&gt;iam-overpermission-wildcard&lt;/code&gt; example shows the dual case from the other side: the heuristic boolean stays silent on a prefix wildcard while Z3 finds the production write. Together the two extensions sketch what a sound + complete checker looks like at the boundary between "policy is broad" and "the resulting attack chain is reachable" — different questions, different provers, both necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deny-list refutation
&lt;/h2&gt;

&lt;p&gt;The natural defensive move after reading Rhino's research is to write a Deny policy listing all 21 actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"iam:CreatePolicyVersion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iam:SetDefaultPolicyVersion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"iam:AttachUserPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iam:AttachGroupPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iam:AttachRolePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"iam:PutUserPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iam:PutGroupPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iam:PutRolePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"iam:AddUserToGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"iam:CreateAccessKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iam:CreateLoginProfile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"iam:UpdateLoginProfile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iam:UpdateAssumeRolePolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ec2:RunInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"lambda:CreateFunction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lambda:UpdateFunctionCode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"lambda:CreateEventSourceMapping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"glue:CreateDevEndpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"glue:UpdateDevEndpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"cloudformation:CreateStack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"datapipeline:CreatePipeline"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-run the prover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;====================================================================
== partial-deny (deny covers Rhino's 21 actions)
====================================================================

--- Pattern 1: Policy Self-Mutation ---
  reachable: 1 / 13 methods
    [NEW    ] iam:DeleteRolePermissionsBoundary

--- Pattern 2: Credential Creation / Theft ---
  reachable: 3 / 7 methods
    [NEW    ] iam:CreateVirtualMFADevice + iam:EnableMFADevice
    [NEW    ] iam:DeactivateMFADevice
    [NEW    ] sts:GetFederationToken

--- Pattern 3: Compute + PassRole ---
  reachable: 9 / 17 methods
    [NEW    ] autoscaling:CreateLaunchConfiguration + autoscaling:CreateAutoScalingGroup
    [NEW    ] ecs:RunTask
    [NEW    ] ecs:CreateService
    [NEW    ] codebuild:CreateProject + codebuild:StartBuild
    [NEW    ] sagemaker:CreateNotebookInstance
    [NEW    ] sagemaker:CreateTrainingJob
    [NEW    ] batch:SubmitJob
    [NEW    ] states:CreateStateMachine
    [NEW    ] apprunner:CreateService

--- Pattern 4: Indirect Compute Invocation ---
  reachable: 10 / 10 methods
    [Rhino 16] dynamodb:PutItem (the partial deny didn't include this)
    [NEW    ] sqs:SendMessage
    [NEW    ] sns:Publish
    [NEW    ] s3:PutObject
    [NEW    ] events:PutRule + events:PutTargets
    [NEW    ] iot:CreateTopicRule
    [NEW    ] ses:CreateReceiptRule
    [NEW    ] cognito-idp:UpdateUserPool
    [NEW    ] cloudwatch:PutMetricAlarm
    [NEW    ] kinesis:PutRecord

--- Pattern 5: Role Trust Modification ---
  reachable: 1 / 3 methods
    [NEW    ] iam:DeleteRolePolicy

--- Cross-pattern summary ---
  reachable:      24 methods
  Rhino's 21 hit: 1 / 21
  beyond Rhino:   23 methods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;24 reachable methods after the defender did everything Rhino's research told them to. The deny-list approach catches 20 of 21 Rhino-numbered methods (Rhino 16's &lt;code&gt;dynamodb:PutItem&lt;/code&gt; slipped through because the defender thought of it as a data-write action, not a compute-trigger). The 27 additional methods Z3 finds — the autoscaling chain, the ECS chain, the SQS trigger, the CloudWatch alarm — those weren't on Rhino's list, so they aren't on the defender's deny list, so they aren't blocked.&lt;/p&gt;

&lt;p&gt;That's the proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why structural beats enumeration
&lt;/h2&gt;

&lt;p&gt;AWS adds roughly one new compute service per quarter. Each new compute service is a potential new entry in Pattern 3's registry. Each new event source is a potential new entry in Pattern 4. The defender's deny list grows monotonically. Their research budget does not.&lt;/p&gt;

&lt;p&gt;A structural query asks "is there &lt;em&gt;any&lt;/em&gt; method in this shape that the principal can execute?" The query itself doesn't change as new services launch. The &lt;em&gt;registry&lt;/em&gt; changes — one new line per new service. The Z3 evaluation runs in milliseconds. The cost of extending coverage is one struct entry plus a recompile.&lt;/p&gt;

&lt;p&gt;A heuristic enumeration asks "does the principal have permission to call this specific action?" The heuristic itself is correct for what it checks. The &lt;em&gt;coverage&lt;/em&gt; is fragile. Each new method requires a new predicate, control YAML and test. The defender's catalogue grows at a rate determined by research bandwidth. AWS's API surface grows at a rate determined by AWS's product roadmap. These rates are not comparable.&lt;/p&gt;

&lt;p&gt;This is Lambert's argument applied to defense: &lt;strong&gt;enumeration loses to a growing surface; verification proves coverage within a model.&lt;/strong&gt; The model is the five patterns. New methods that fit any of those five shapes get coverage automatically. Methods that don't fit any shape — a brand new attack class — require a sixth pattern, which is also one query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The remediated case
&lt;/h2&gt;

&lt;p&gt;Least privilege closes everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;====================================================================
== remediated (least-privilege)
====================================================================

--- Pattern 1: Policy Self-Mutation ---           UNSAT
--- Pattern 2: Credential Creation / Theft ---     UNSAT
--- Pattern 3: Compute + PassRole ---              UNSAT
--- Pattern 4: Indirect Compute Invocation ---     UNSAT
--- Pattern 5: Role Trust Modification ---         UNSAT

--- Cross-pattern summary ---
  reachable:      0 methods
  Rhino's 21 hit: 0 / 21
  beyond Rhino:   0 methods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The remediated principal has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No self-mutation actions (no &lt;code&gt;iam:Attach*&lt;/code&gt;, &lt;code&gt;iam:Put*&lt;/code&gt;, &lt;code&gt;iam:Create*&lt;/code&gt; against IAM resources).&lt;/li&gt;
&lt;li&gt;No credential-modification actions on other principals.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iam:PassRole&lt;/code&gt; scoped to one specific role ARN.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;iam:UpdateAssumeRolePolicy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The roles it can pass to are not admin-equivalent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All five patterns return UNSAT. There is no escalation path within the modeled space.&lt;/p&gt;

&lt;p&gt;The least-privilege configuration of the remediation checklist looks like &lt;em&gt;in mathematical form&lt;/em&gt;: scope the resource ARNs, drop the self-mutation actions, refuse the broad &lt;code&gt;iam:PassRole&lt;/code&gt; grant, and the SAT solver returns UNSAT on every pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What pattern-matching tools see
&lt;/h2&gt;

&lt;p&gt;Run a per-technique scanner against the rhino-vulnerable principal. The output is 21 separate findings: "principal has CreatePolicyVersion," "principal has AttachUserPolicy," "principal has RunInstances + PassRole," etc. Each is correct. None of them composes the 21 findings into "and here are the 27 &lt;em&gt;other&lt;/em&gt; paths in the same shape that aren't on your radar."&lt;/p&gt;

&lt;p&gt;Run the per-technique scanner against partial-deny. The output is fewer findings. The deny closes most of them. The output reads as a successful remediation. But the 24 paths Z3 still finds aren't in the scanner's list, so they don't appear in the report. The defender ships the change, runs the scan, sees the green dashboard, and concludes the work is done.&lt;/p&gt;

&lt;p&gt;The math says otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architectural rule
&lt;/h2&gt;

&lt;p&gt;Don't build deny lists of known privesc actions. Build allow lists of known-needed actions, scoped by resource ARN, and rely on the implicit deny for everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What the defender wrote (deny-list — fragile):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="c1"&gt;...&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;actions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Rhino&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;enumerated&lt;/span&gt;&lt;span class="c1"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// What math suggests instead (allow-list — sound):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::data-team-bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::data-team-bucket/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iam:PassRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::*:role/data-team-job-role"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first form is a list of known bad. It grows. It trails AWS's product roadmap. It's never complete. The second form is a list of known good. It grows only when the team's actual needs grow. Everything else is implicitly denied. Patterns 1, 2, 4, 5 don't fire because the actions aren't in the allow list. Pattern 3 doesn't fire because &lt;code&gt;iam:PassRole&lt;/code&gt; is scoped to a non-admin role.&lt;/p&gt;

&lt;p&gt;Z3 returns UNSAT on all five patterns. That's not "we caught the methods we knew about." It's "no method in the modeled space exists." Different proposition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;IAM identity policies use Allow-with-resource-ARN-scoping rather than Deny-with-action-blocklist&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iam:PassRole&lt;/code&gt; resource is a list of specific role ARNs, not &lt;code&gt;Resource: "*"&lt;/code&gt; with a service condition&lt;/li&gt;
&lt;li&gt;No principal has self-mutation actions (&lt;code&gt;iam:Attach*&lt;/code&gt;, &lt;code&gt;iam:Put*&lt;/code&gt; on IAM resources, &lt;code&gt;iam:DeleteRolePermissionsBoundary&lt;/code&gt;, &lt;code&gt;iam:UpdateAssumeRolePolicy&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;No principal has credential-modification actions on other principals (&lt;code&gt;iam:CreateAccessKey&lt;/code&gt;, &lt;code&gt;iam:UpdateLoginProfile&lt;/code&gt;, MFA-related actions on roles other than self)&lt;/li&gt;
&lt;li&gt;When AWS announces a new compute service, the org's posture review treats it as a Pattern 3 candidate by default — the registry gets a new entry, the prover runs against existing principals, every match is a finding&lt;/li&gt;
&lt;li&gt;CI runs a pattern-based prover (this article's template) against post-deploy observation snapshots, not just per-technique heuristic checks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The meta-lesson
&lt;/h2&gt;

&lt;p&gt;Rhino did the foundational research that made every subsequent IAM privesc tool possible. The ecosystem rallied around the 21 methods. Z3 confirms all 21 of them. It just demonstrates that 21 is a lower bound, not the total. The total is "every method that fits these five shapes," and that number is &lt;em&gt;whatever AWS launches next&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Defenders who think in lists will always be one quarter behind AWS's release calendar. Defenders who think in shapes are bounded by their model and the model is small enough to keep current. Five patterns. New service launches? Add it to the right registry. Re-run the prover. Done.&lt;/p&gt;

&lt;p&gt;Rhino found 21. Z3 found 50. 24 of those still work after blocking everything Rhino listed. That's six sentences and the case for thinking structurally.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The example shipped is at &lt;a href="https://github.com/sufield/stave/tree/main/examples/iam-21-privesc-5-patterns" rel="noopener noreferrer"&gt;&lt;code&gt;stave/examples/iam-21-privesc-5-patterns/&lt;/code&gt;&lt;/a&gt; — a CEL evaluation via &lt;code&gt;pkg/stave.Apply&lt;/code&gt; (scoped to one of Stave's 44+ per-technique IAM escalation controls; fires on the vulnerable and partial-deny fixtures, silent on remediated) and a Z3 SAT prover that runs the five pattern queries across all three fixtures and prints the cross-pattern summary quoted in this article. The Z3 binary lives in a sibling Go module so its libz3 link stays out of Stave's main vendored tree. &lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;Stave&lt;/a&gt; detects this pattern and 31 other H1-grounded scenarios from local AWS configuration snapshots, with no cloud credentials.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>aws</category>
      <category>cloud</category>
      <category>appsec</category>
    </item>
    <item>
      <title>AI Solved the Easy Part of Programming. The Hard Part is Now Harder to See.</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Thu, 16 Jul 2026 11:45:34 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/ai-solved-the-easy-part-of-programming-the-hard-part-is-now-harder-to-see-56l8</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/ai-solved-the-easy-part-of-programming-the-hard-part-is-now-harder-to-see-56l8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In 1986, Fred Brooks published "No Silver Bullet — Essence and Accident in Software Engineering." His argument: software complexity has two kinds. Accidental complexity is the difficulty imposed by the tools — syntax rules, boilerplate code, build configurations, deployment scripts. Essential complexity is the difficulty imposed by the problem. The business rules, the data relationships, the edge cases where the real world doesn't fit neatly into a data model.&lt;/p&gt;

&lt;p&gt;Brooks predicted that even if we eliminate ALL accidental complexity, software will still be hard. Essential complexity is irreducible. It comes from the domain, not the tools. No tool can eliminate it because it's not a property of the tool. It's a property of the problem.&lt;/p&gt;

&lt;p&gt;AI is the most effective tool ever built for eliminating accidental complexity. It's proving Brooks right in the way he predicted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI eliminates
&lt;/h2&gt;

&lt;p&gt;AI generates boilerplate instantly. HTTP handlers, database CRUD operations, serialization code, test scaffolding, configuration files, Dockerfile templates, CI pipeline definitions. Every piece of accidental complexity that developers spend hours writing — AI produces in seconds.&lt;/p&gt;

&lt;p&gt;A developer who spends 40% of their time on boilerplate and 60% on essential logic now spends 5% on boilerplate and has 95% of their time for essential logic. The time savings and productivity improvement is real. The 10x feeling is real for the accidental part.&lt;/p&gt;

&lt;p&gt;AI eliminates accidental complexity. The problem happens after the accidental complexity is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What remains after the accidental is removed
&lt;/h2&gt;

&lt;p&gt;When the boilerplate is generated in seconds, the developer reaches the essential complexity faster. The business logic, domain rules, edge cases etc. The relationships between entities that don't map clearly to database tables. The race conditions that only appear under specific ordering. The authorization rules that depend on organizational hierarchy, delegation chains, and temporal state.&lt;/p&gt;

&lt;p&gt;This is the complexity Brooks said was irreducible. AI can't eliminate it because it's not a syntax problem. It's a problem about the correspondence between the software and the real world. The AI can generate a function that handles a business rule. But it can't know whether the business rule is correct. Because correct is defined by the domain.&lt;/p&gt;

&lt;p&gt;The old workflow had a natural pacing mechanism: the time spent writing boilerplate was time the developer unconsciously spent thinking about the domain. The struggle with syntax gave the developer time to notice: "wait, this business rule doesn't make sense." The boilerplate was annoying, but it was also friction that slowed the developer down enough to think.&lt;/p&gt;

&lt;p&gt;AI removed the friction. The developer goes from prompt to working code in minutes. The essential complexity is still there. But the developer didn't notice it because they never slowed down enough to see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The illusion of completion
&lt;/h2&gt;

&lt;p&gt;The most dangerous consequence: AI-generated code looks complete. The boilerplate and the error handling is there. The tests pass. The developer reviews it and thinks: "this is done."&lt;/p&gt;

&lt;p&gt;But done means the accidental complexity is resolved. The essential complexity — the domain logic, the edge cases, the invariants was never examined. The code handles the common cases correctly (because the AI was trained on common patterns) and fails on the specific cases that make this particular domain unique (because those weren't in the training data).&lt;/p&gt;

&lt;p&gt;The failure surfaces weeks or months later, when a customer hits an edge case the AI never considered. The developer debugging the issue finds code they didn't write, implementing a business rule they didn't design, handling a case they didn't think about. They can't fix it because they don't understand the essential complexity. The AI hid it under a layer of perfect-looking accidental resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reviewer's dilemma
&lt;/h2&gt;

&lt;p&gt;This creates an asymmetry that compounds the problem. It is harder to review 100 lines of AI-generated code than to write 50 lines yourself.&lt;/p&gt;

&lt;p&gt;When a developer writes code, they build a mental model of the domain as they go. They encounter edge cases during implementation, make decisions about how to handle them, and carry the reasoning forward. The code and the understanding are built together.&lt;/p&gt;

&lt;p&gt;When a developer reviews AI-generated code, the code arrives pre-formed with no reasoning trail. The reviewer must reverse-engineer the domain model from the implementation — figure out which edge cases were handled, which were missed, and whether the ones that were handled were handled correctly. This is harder than building the model yourself, because the reviewer is reconstructing intent from an artifact that has no intent.&lt;/p&gt;

&lt;p&gt;The result: teams that measure productivity by lines of code shipped or pull requests merged see massive throughput gains. Teams that measure productivity by defects avoided or domain coverage verified see the same essential-complexity bottleneck, now arriving faster and in larger batches.&lt;/p&gt;

&lt;p&gt;Throughput is not progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the essential complexity lives
&lt;/h2&gt;

&lt;p&gt;Brooks identified four properties that make essential complexity irreducible. AI interacts with each one differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complexity.&lt;/strong&gt; The sheer number of states the system can be in. A system with ten interacting components has more possible states than a developer can enumerate. Brooks called this the fundamental property — the one that makes all the others worse.&lt;/p&gt;

&lt;p&gt;AI multiplies this property directly. A developer writing code by hand produces hundreds of lines per day. Each line is a decision point they consciously made. An AI-assisted developer ships thousands of lines per day. Each line is a decision point someone needs to understand but nobody consciously made. The number of states in the system grows with the code, but the developer's ability to map those states in their head remains constant. More code, same brain. The state space grows. The developer's model of it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conformity.&lt;/strong&gt; The software must conform to the real world — to regulations, to existing systems, to human expectations. These conformity requirements can't be derived from code patterns. They must be understood from the domain.&lt;/p&gt;

&lt;p&gt;Conformity is the hardest of Brooks's four properties, and the cloud makes it visible. The cloud is a conformity engine. You must conform to every service that imposes a model. AWS's IAM evaluation logic, GCP's networking model, Azure's RBAC hierarchy. AI is fluent in the syntax of these APIs but silent on the security implications of how they interact with your internal compliance rules.&lt;/p&gt;

&lt;p&gt;AI that generates IAM policies based on training patterns doesn't understand IAM evaluation semantics. It pattern-matches on policies that appeared in its training data. The difference matters when two policies interact in ways the training data never combined, when a new action namespace silently changes the meaning of an existing wildcard, or when the specific compliance requirement falls between two regulatory interpretations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Changeability.&lt;/strong&gt; The requirements change. Because the real world changes. A pricing model that was correct last quarter is wrong this quarter. An IAM policy that was secure last week grants OAuth agent authorization this week because the provider added new actions to the namespace. The AI-generated code implements last quarter's model perfectly. The developer who needs to update it for this quarter's model doesn't understand the old model (they didn't write it) and can't safely modify it (they don't know which assumptions are embedded in the implementation).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invisibility.&lt;/strong&gt; Software is invisible. There's no spatial representation that shows the full structure. The developer must hold the structure in their head. As Peter Naur argued in "Programming as Theory Building," the developer must build a &lt;em&gt;theory&lt;/em&gt; of the program: a mental model of how the solution maps to the problem, what each piece does and why, how the parts relate to each other and to the domain.&lt;/p&gt;

&lt;p&gt;Naur's insight compounds Brooks's. If programming is building a theory in the developer's head, then AI-generated code is theory-less code. The artifact exists but the theory behind it was never constructed. No one holds the mental model of why the code is shaped the way it is, which domain constraints it satisfies, or which assumptions would break if the requirements changed. The code is doubly invisible: the developer doesn't have the theory, and the code's structure follows patterns from training data rather than patterns from the domain. The structure in the code doesn't match the structure of the problem. It matches the structure of similar code the AI has seen.&lt;/p&gt;

&lt;p&gt;This makes AI-generated code uniquely fragile under change. Human-written code degrades when the developer who understood it leaves the team — the theory walks out the door. AI-generated code starts without a theory. There is no door for it to walk out of. The understanding was never there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for AI-assisted development
&lt;/h2&gt;

&lt;p&gt;Brooks's insight predicts what DORA's 2025 data measured: organizations saw roughly a 10% improvement in actual code shipped to production. Higher throughput alongside higher instability — more code shipped, more rollbacks.&lt;/p&gt;

&lt;p&gt;The accidental complexity was eliminated (massive time savings). The essential complexity remained (same number of domain bugs). The net improvement was modest. Because the time was always going to be spent in essential complexity, and AI doesn't reduce it.&lt;/p&gt;

&lt;p&gt;The teams that will succeed are the ones that use the time saved on accidental complexity to invest more time in essential complexity — in domain modeling, property definition, invariant specification, understanding the business rules deeply enough to verify that the AI's output is correct.&lt;/p&gt;

&lt;p&gt;The teams that will fail are the ones that use the time savings to generate more code, features, endpoints, services without investing in understanding the domain. They'll hit Brooks's wall later, with a larger codebase, less understanding, and more surface area for essential-complexity failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  The specification as essential complexity management
&lt;/h2&gt;

&lt;p&gt;Specifications — type signatures, API contracts, database schemas, property definitions are the tools for managing essential complexity. They make the domain rules explicit instead of leaving them implicit in the code.&lt;/p&gt;

&lt;p&gt;A type signature that says &lt;code&gt;func ProcessOrder(order Order) (Receipt, error)&lt;/code&gt; captures essential complexity: an order produces a receipt or an error. Not a maybe-receipt, a silent failure or a partial result. The type system enforces this at compile time regardless of whether the implementation was written by a human or an AI.&lt;/p&gt;

&lt;p&gt;A property that says "no order can have a negative total after discounts are applied" captures a domain invariant. AI might generate discount logic that produces negative totals for specific discount combinations. The property catches it — mechanically, deterministically, on every change. Because the essential complexity was made explicit as a verifiable statement.&lt;/p&gt;

&lt;p&gt;AI eliminates accidental complexity. Specifications manage essential complexity. Both are needed. AI without specifications produces code that looks correct but embeds unexamined domain assumptions. Specifications without AI require humans to write all the boilerplate. Together: the AI handles the syntax, the specifications handle the semantics.&lt;/p&gt;

&lt;p&gt;Brooks said there's no silver bullet. He was right. AI eliminates the accidental complexity that was never the hard part. The hard part remains — irreducible, essential, and now hidden under a layer of perfectly formatted code that looks like it's finished.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;In cloud security, the essential complexity is the domain invariant: "no privilege escalation path exists through any combination of roles, policies, and trust relationships." The accidental complexity is writing the checks — the YAML, the CEL predicates, the test fixtures. &lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;Stave&lt;/a&gt; handles the accidental complexity (3,000+ controls, mechanically evaluated). The essential complexity, knowing which properties to verify remains a human judgment that no AI can replace. Brooks, 40 years later is still right.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Auto-Fix is not the Problem. The Signal is the Problem.</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Wed, 15 Jul 2026 11:59:33 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/auto-fix-is-not-the-problem-the-signal-is-the-problem-1lh0</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/auto-fix-is-not-the-problem-the-signal-is-the-problem-1lh0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Harry Wetherald (Maze) makes a case against auto-fixing vulnerabilities: most findings are false positives, every code or cloud change carries risk. So auto-fixing everything means introducing risk for zero security benefit. His proposed path: first improve understanding (AI investigates findings in context), then generate fix suggestions for humans, then eventually automate the fixes once they're reliable.&lt;/p&gt;

&lt;p&gt;The problem identification is correct. The solution addresses the wrong layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What he gets right
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The false positive rate is real. The answers range from 80% to 99%. Most findings don't matter when investigated in the context of the specific environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Auto-fixing false positives is worse than ignoring them. Every change to cloud or code carries risk such as downtime, new attack surface, regression. If 90% of findings are false positives, auto-fixing all of them means 90% of changes are risk for zero benefit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI-generated fix suggestions without context are not helpful. If the finding lacks organizational context, the fix suggestion will also lack the context. You get a wall of auto-fixes to review instead of a wall of findings to review. The shape of the problem changes; the size doesn't.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These observations are accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the solution misses
&lt;/h2&gt;

&lt;p&gt;The proposed progression: better AI investigation → fix suggestions → human review → eventually full automation.&lt;/p&gt;

&lt;p&gt;This path accepts the scanner's output a finding, a signal as the starting point and tries to improve what happens after. It never questions the output itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The false positive problem is two problems
&lt;/h3&gt;

&lt;p&gt;Wetherald's "80-99% false positives" combines two different things:&lt;/p&gt;

&lt;p&gt;The first is false positives. The scanner is wrong about the configuration. The bucket does have versioning. The role doesn't have that permission. The scanner missed something. The fix for this is straightforward: fix the scanner. Better rules, parsing and coverage. This is an engineering problem with engineering solutions.&lt;/p&gt;

&lt;p&gt;The second is true findings that aren't exploitable. The configuration is as the scanner says. The bucket really does lack versioning. But the context makes exploitation impossible: the bucket is empty, internal-only, behind a VPC endpoint policy, and protected by an SCP. The finding is true. The risk is zero.&lt;/p&gt;

&lt;p&gt;These require different solutions. The first needs better detection. The second needs something else entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context is not an AI problem
&lt;/h3&gt;

&lt;p&gt;The context that makes a true finding non-exploitable is the configuration graph. The relationships between resources, the organizational policies, the permission boundaries, the network topology. This context is static. It's deterministic. It's already in the cloud provider's API responses.&lt;/p&gt;

&lt;p&gt;Wetherald proposes AI that "investigates findings in the context of your environment." But the context he describes is not ambiguous. The SCP either blocks the action or it doesn't. The VPC endpoint policy either restricts the path or it doesn't. The permission boundary either limits the role or it doesn't.&lt;/p&gt;

&lt;p&gt;These are not judgment calls requiring AI investigation. They are predicates evaluable against a configuration snapshot. The answer is true or false. Same snapshot, predicate and answer, every time.&lt;/p&gt;

&lt;p&gt;The reason scanners produce findings without this context is that scanners evaluate individual resources, not the relationships between them. A scanner checks: "does this bucket have versioning?" That's a property of one resource. Whether that bucket's lack of versioning matters depends on what writes to it, who can access it, what policies protect it, and whether it participates in an attack path. Those are relationships — edges in a graph, not properties of nodes.&lt;/p&gt;

&lt;p&gt;The false positive is an architecture failure. The tool evaluates nodes. The risk lives in edges.&lt;/p&gt;

&lt;h3&gt;
  
  
  The output type is the root cause
&lt;/h3&gt;

&lt;p&gt;A scanner produces a finding — "this bucket lacks versioning." That's a signal. It requires human interpretation before a machine can act on it. Is this finding real? Does it matter? Should we fix it? Those questions exist because the output is a signal.&lt;/p&gt;

&lt;p&gt;Consider a different output: a verdict. "This configuration is NON_COMPLIANT with respect to control X, evaluated against predicate Y, from snapshot Z. Exit code 3." That's a decision. A pipeline reads the exit code. No interpretation, triage or "is this real?" question.&lt;/p&gt;

&lt;p&gt;The false positive problem Wetherald describes is a property of signals, not a property of security evaluation. Signals require investigation because they're incomplete. They describe one resource without the graph. Verdicts don't require investigation because they evaluate the graph and produce a deterministic result.&lt;/p&gt;

&lt;p&gt;Better AI investigating signals is still AI interpreting signals. The output is still probabilistic — "I'm 95% confident this matters." That's still a signal. A faster, smarter signal is still a signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  The missing step: verification
&lt;/h3&gt;

&lt;p&gt;Wetherald's path is: find → investigate → suggest fix → human executes fix. The fix happens. Then what?&lt;/p&gt;

&lt;p&gt;How does the defender know the fix worked? How does the auto-fixer know the fix resolved the vulnerability rather than introducing a different one?&lt;/p&gt;

&lt;p&gt;The fix-verification loop is absent from his progression. Without verification, "auto-fix" means "auto-change and hope." With verification:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finding identified (the verdict says NON_COMPLIANT)&lt;/li&gt;
&lt;li&gt;Fix applied (by human or automation)&lt;/li&gt;
&lt;li&gt;Re-evaluate (capture new snapshot, re-run evaluation)&lt;/li&gt;
&lt;li&gt;Verdict changes (NON_COMPLIANT → COMPLIANT, or it doesn't)&lt;/li&gt;
&lt;li&gt;If it doesn't change: the fix was wrong. Try again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verification makes auto-fix safe. Not better AI investigation or fix suggestions. Verification. A deterministic verdict engine that can confirm the forbidden state is gone after the fix is applied.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-healing is the wrong endpoint
&lt;/h3&gt;

&lt;p&gt;Wetherald's vision: AI agents find, investigate, and fix without human involvement. Self-healing environments.&lt;/p&gt;

&lt;p&gt;This assumes the bottleneck is human labor. Too many findings for humans to review, so automate the humans away.&lt;/p&gt;

&lt;p&gt;The actual bottleneck is signal quality. If 90% of findings are false positives, the problem isn't that humans are slow. It's that the tool is producing 9 units of noise for every 1 unit of signal. Faster humans (or AI replacing humans) processing the same noise doesn't solve the problem. It processes noise faster.&lt;/p&gt;

&lt;p&gt;The alternative is changing the output so the question "is this real?" doesn't arise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A deterministic verdict doesn't need investigation. The predicate either matches the configuration or it doesn't.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An exploitability classification (exploitable / one change away / reachable only) doesn't need triage. The graph evaluation already identified which findings have every precondition present for an attack.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A fix-verification loop doesn't need hope. The verdict engine confirms the forbidden state is gone.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these require AI. They require a different tool architecture: one that evaluates the configuration graph deterministically, produces verdicts instead of signals, and can verify that a fix resolved the finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layer error
&lt;/h2&gt;

&lt;p&gt;Wetherald is building a better consumer of signals. That's useful if your tools produce signals, you need something to interpret them, and AI interpretation is better than human interpretation at scale.&lt;/p&gt;

&lt;p&gt;But building a better signal consumer doesn't fix the fact that the signal is the wrong output type. It's optimizing the layer above the problem rather than fixing the layer where the problem lives.&lt;/p&gt;

&lt;p&gt;The layer where the problem lives is the tool's primary function. A scanner that evaluates nodes will always produce findings that lack graph context. No amount of post-hoc AI investigation changes what the scanner evaluated. The investigation is reconstructing the graph that the scanner should have evaluated in the first place — expensively, probabilistically, differently each time.&lt;/p&gt;

&lt;p&gt;A tool that evaluates the graph from the start doesn't produce the false positive to investigate. The "80-99% false positive rate" is a consequence of evaluating nodes. Evaluate edges and the rate changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right path
&lt;/h2&gt;

&lt;p&gt;Wetherald's path: find (signal) → investigate (AI) → suggest fix → human review → auto-fix → self-healing.&lt;/p&gt;

&lt;p&gt;The alternative: evaluate (verdict) → classify (exploitable / one-away / reachable) → fix → verify (re-evaluate, verdict changes) → automate verified fixes.&lt;/p&gt;

&lt;p&gt;The difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;No investigation step. The verdict is the investigation. The predicate evaluated the graph and produced a deterministic result.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No triage step. The exploitability classification is the triage. Exploitable findings first. One-away findings next (protect the precondition before it flips). Reachable findings last.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verification built in. Every fix is re-evaluated. The verdict either changes (fix worked) or it doesn't (fix was wrong). No "auto-fix and hope."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Auto-fix becomes safe when the fix is verifiable. Not when AI gets better at investigation. The deterministic verdict tells the auto-fixer what needs to change, and the re-evaluation confirms the change worked.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This path doesn't require AI to improve. It requires the tool to produce a different output. The AI era's contribution to security isn't better investigation of signals. It's recognizing that investigation is a workaround for the wrong output type.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;Stave&lt;/a&gt; produces verdicts, not signals. Deterministic evaluation of configuration snapshots against a control catalog. Exploitability classification on every finding. Fix verification via snapshot diff. The false positives Wetherald describes don't arise, because they evaluate the graph. &lt;code&gt;stave apply --observations ./your-snapshot/&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>security</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Four Eras of Cloud Security. Same Verb.</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Tue, 14 Jul 2026 09:36:35 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/four-eras-of-cloud-security-same-verb-3kdb</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/four-eras-of-cloud-security-same-verb-3kdb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Scott Piper published a twenty-year retrospective on cloud security research in March 2026. It's the most useful structural history of the field I've seen — four eras, each with defining milestones, each with the tools and research that shaped cloud security. If you work in cloud security, read it first.&lt;/p&gt;

&lt;p&gt;What follows is a question about what the history reveals when you examine one detail it doesn't discuss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four eras
&lt;/h2&gt;

&lt;p&gt;Piper divides two decades into four eras:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2006–2016, Foundational.&lt;/strong&gt; Cloud providers built the security primitives — IAM (2011), CloudTrail (2013), Organizations and SCPs (2016). Before these existed, there was no mechanism for least privilege, no audit trail, and no organizational boundary. Security research in this era was part-time work from people with broader careers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2016–2021, CSPM.&lt;/strong&gt; Cloud security became a full-time job. CIS Benchmarks standardized what to check. Open-source tools proliferated — Prowler, CloudMapper, Pacu, Cloud Custodian, ScoutSuite. Cloud security during this time largely meant deploying a CSPM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2021–2025, CNAPP.&lt;/strong&gt; Point solutions gave way to platforms. Vendors integrated CSPM with container scanning, vulnerability management, and workload protection into a single product category. Research teams at vendors began finding cross-tenant vulnerabilities in the cloud providers themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2025–present, AI.&lt;/strong&gt; AI accelerates both attack and defense. Exploits that required deep language expertise are generated in minutes. A CTF challenge was solved by an AI within minutes of release. The industry is speed-running the cloud eras.&lt;/p&gt;

&lt;p&gt;This is a well-evidenced narrative. Every era is defined by a change in what tools could do and who was building them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verb that didn't change
&lt;/h2&gt;

&lt;p&gt;Look at what each era's defining tools do. The direct action each tool performs on its direct object.&lt;/p&gt;

&lt;p&gt;In the CSPM era, the defining tools match API responses against rule databases. Prowler, ScoutSuite and Cloud Custodian matches. The CIS Benchmark is a rule database. The verb is &lt;strong&gt;match&lt;/strong&gt;, and the output is a finding.&lt;/p&gt;

&lt;p&gt;In the CNAPP era, the defining tools aggregate findings from multiple scanners into a single platform. The CNAPP collects what the CSPM found, what the vulnerability scanner found, what the container scanner found, and deduplicates. The verb is &lt;strong&gt;aggregate&lt;/strong&gt;, and the output is a consolidated finding.&lt;/p&gt;

&lt;p&gt;In the AI era, the defining tools score events against learned baselines, or exploit vulnerabilities faster. The verb is &lt;strong&gt;score&lt;/strong&gt; or &lt;strong&gt;exploit&lt;/strong&gt;, and the output is a detection or a proof-of-concept.&lt;/p&gt;

&lt;p&gt;Three eras of tooling. Three different verbs. But the output is always the same category: &lt;strong&gt;signals&lt;/strong&gt;. A finding for human interpretation. An alert for human triage. A score for human thresholding. A detection for human review.&lt;/p&gt;

&lt;p&gt;No era introduced a tool whose output is a &lt;strong&gt;decision&lt;/strong&gt;. A deterministic, machine-verifiable, per-asset verdict that a pipeline can act on without human interpretation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why signals aren't decisions
&lt;/h2&gt;

&lt;p&gt;A finding says: "this S3 bucket lacks versioning." A human must decide whether that matters, whether it's a true positive, and what to do about it.&lt;/p&gt;

&lt;p&gt;A verdict says: "this configuration is COMPLIANT or NON_COMPLIANT with respect to this control, and here is the evaluation trace." A machine reads the exit code and gates the pipeline. No interpretation, triage or threshold is required from a human.&lt;/p&gt;

&lt;p&gt;A CSPM finding can be accurate. The difference is in what happens next. A signal enters a queue. A verdict enters a pipeline. Signals require human interpretation as a necessary step between detection and action. Verdicts are the action.&lt;/p&gt;

&lt;p&gt;Every era improved signals. CSPMs made signals more comprehensive. CNAPPs made signals more consolidated. AI makes signals faster. But improving a signal doesn't change it. A faster finding is still a finding. A better-correlated alert is still an alert. A higher-accuracy score is still a score.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the history is silent
&lt;/h2&gt;

&lt;p&gt;Piper's history is comprehensive within its scope. The omission is structural. Deterministic verification tools don't appear in the history because they don't appear in the field. The category doesn't exist.&lt;/p&gt;

&lt;p&gt;The cloud security industry has produced hundreds of tools across four eras. Billions of dollars of venture capital. Thousands of dedicated security engineers. Every tool produces signals.&lt;/p&gt;

&lt;p&gt;The question is: is this because signals are sufficient, or because the architecture of every tool category makes decisions structurally impossible?&lt;/p&gt;

&lt;h2&gt;
  
  
  The architectural constraint
&lt;/h2&gt;

&lt;p&gt;A CSPM queries the live cloud API, matches the response against a rule, and produces a finding. The finding is a signal because the CSPM evaluates one resource at a time. It can say "this bucket lacks versioning" but cannot say "this bucket is the destination for your CloudTrail trail, and this role has permission to delete it, and no SCP blocks cross-account writes, and therefore a complete attack path exists." The compound judgment requires evaluating relationships between resources, not properties of individual resources.&lt;/p&gt;

&lt;p&gt;A CNAPP inherits this limitation. It aggregates findings from CSPMs and scanners, but aggregation doesn't create compound reasoning. It creates compound noise. More findings from more scanners, consolidated into one dashboard, still evaluated one resource at a time.&lt;/p&gt;

&lt;p&gt;An AI-powered tool can reason across resources in principle, but introduces a different problem: non-determinism. The same input can produce different outputs across runs. For a security tool whose findings may be presented to auditors, regulators, or courts, the finding must be reproducible. A probabilistic verdict is a contradiction in terms.&lt;/p&gt;

&lt;p&gt;The architectural gap is specific: no tool category evaluates the configuration graph (relationships between resources, not just properties of resources) deterministically (same input, same output, every time) from a coherent snapshot (point-in-time capture, not a stream of events).&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing verb
&lt;/h2&gt;

&lt;p&gt;The history has four eras of verbs: match, aggregate, score, exploit. The missing verb is &lt;strong&gt;evaluate&lt;/strong&gt; — produce a deterministic categorical verdict per asset by applying an enumerated catalog of controls to a snapshot of cloud state.&lt;/p&gt;

&lt;p&gt;The distinction is operational:&lt;/p&gt;

&lt;p&gt;A tool that &lt;strong&gt;matches&lt;/strong&gt; takes an API response and a rule, and outputs a finding. The finding enters a triage queue.&lt;/p&gt;

&lt;p&gt;A tool that &lt;strong&gt;evaluates&lt;/strong&gt; takes a configuration snapshot and a control catalog, and outputs a verdict. The verdict enters a pipeline as an exit code.&lt;/p&gt;

&lt;p&gt;The input is different (snapshot vs API response). The substrate is different (enumerated catalog vs rule database). The output is different (verdict vs finding). The downstream consumer is different (pipeline vs human).&lt;/p&gt;

&lt;p&gt;This is a different primary function that produces a different output type. In the same way that CNAPP was not a better CSPM but a platform that aggregated them, the missing tool is not a better version of any existing category. It occupies a structural gap in what the toolchain produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compound gap
&lt;/h2&gt;

&lt;p&gt;The sharpest illustration of the gap is compound risk. Consider a specific scenario documented by Unit 42 in June 2026: an attacker who can delete an S3 bucket that serves as a CloudTrail destination can recreate the bucket under their own account, silently rerouting security telemetry to attacker-controlled storage. No credentials stolen or network exploitation. Pure configuration-graph exploitation.&lt;/p&gt;

&lt;p&gt;Detecting this from individual resource properties is impossible. The bucket's own properties are fine. The IAM role's own properties are fine. The CloudTrail trail's own properties are fine. The vulnerability exists only in the relationship between them. The fact that the role can delete the bucket, the trail depends on the bucket, and no organizational policy prevents cross-account writes.&lt;/p&gt;

&lt;p&gt;A CSPM checking each resource individually sees three compliant resources. A CNAPP aggregating those three findings sees three compliant resources. Neither can see the graph. The compound chain — identity → permission → destination → trail → missing data perimeter is invisible to any tool that evaluates nodes without evaluating edges.&lt;/p&gt;

&lt;p&gt;This is not a coverage gap (add more rules). It's an architectural gap. The tool's primary function cannot express the finding. The missing verb — evaluate a snapshot against a catalog — operates on the graph, not on individual resources. The compound chain is a natural output of graph evaluation, and a structural impossibility for resource-level matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for the AI era
&lt;/h2&gt;

&lt;p&gt;Piper frames the AI era around speed: faster exploits, faster patching, faster detection. AI solved a CTF in minutes. AI generated an exploit in 10 minutes. The implicit thesis: the bottleneck is speed, and AI removes it.&lt;/p&gt;

&lt;p&gt;But if the output type hasn't changed, if AI produces faster signals rather than different outputs, then the bottleneck isn't speed. It's the gap between signals and decisions. A faster finding still enters a triage queue. A faster alert still requires human interpretation. A faster score still needs a threshold.&lt;/p&gt;

&lt;p&gt;AI applied to the existing toolchain makes each verb faster. AI-powered matching. AI-powered aggregation. AI-powered scoring. The verbs stay the same. The output stays the same. The signals arrive faster, in greater volume, with higher confidence and still require a human to close the loop.&lt;/p&gt;

&lt;p&gt;The alternative is not AI replacing the human. It's changing the output type so the loop doesn't need closing. A deterministic verdict, mechanically evaluated, same answer every time, with an auditable evaluation trace. That's a tool whose output a pipeline can act on directly, at any speed, because the human isn't in the loop between detection and action.&lt;/p&gt;

&lt;p&gt;That's not the AI era's contribution. That's the contribution of a different verb.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fifth column
&lt;/h2&gt;

&lt;p&gt;Piper's history is a four-column table. Each column is an era. Each era has tools, verbs, and outputs. Adding a fifth column doesn't extend the table. It changes what the table measures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Foundational&lt;/th&gt;
&lt;th&gt;CSPM&lt;/th&gt;
&lt;th&gt;CNAPP&lt;/th&gt;
&lt;th&gt;AI&lt;/th&gt;
&lt;th&gt;?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Defining tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IAM, CloudTrail, SCPs&lt;/td&gt;
&lt;td&gt;Prowler, ScoutSuite, Cloud Custodian&lt;/td&gt;
&lt;td&gt;Wiz, Orca, Prisma Cloud&lt;/td&gt;
&lt;td&gt;AI-powered scanners, agentic exploits&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Verb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(primitives, not tools)&lt;/td&gt;
&lt;td&gt;Match&lt;/td&gt;
&lt;td&gt;Aggregate&lt;/td&gt;
&lt;td&gt;Score&lt;/td&gt;
&lt;td&gt;Evaluate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Live API&lt;/td&gt;
&lt;td&gt;Live API + scanners&lt;/td&gt;
&lt;td&gt;Events + logs&lt;/td&gt;
&lt;td&gt;Snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What advances&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What's possible&lt;/td&gt;
&lt;td&gt;What's checked&lt;/td&gt;
&lt;td&gt;What's consolidated&lt;/td&gt;
&lt;td&gt;How fast&lt;/td&gt;
&lt;td&gt;What's decided&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first four columns improve along the same axis: better, more, faster signals. The fifth column is orthogonal. It doesn't make signals better. It produces a different output type.&lt;/p&gt;

&lt;p&gt;The history doesn't mention this column because nobody has built it. The field went from matching to aggregating to scoring without ever stopping to ask: what if the output wasn't a signal?&lt;/p&gt;




&lt;p&gt;When you remove humans in the loop where they are slow and the weakest link in the system, they move to a different loop where the expert judgement is encoded in controls that can be evaluated at machine speed without slowing down the Agentic era development loop. Slow and deliberate when human expertise is required. Fast and accurate when agents are required. Two different loops that serve diffferent purpose.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;That question is answered by &lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;Stave&lt;/a&gt;. The verb is evaluate. The input is a configuration snapshot. The output is a deterministic per-asset verdict with an auditable evaluation trace. The compound chain in the Unit 42 example — identity with delete permission on a CloudTrail destination bucket, no SCP data perimeter — is a finding Stave produces and no signal-producing tool structurally can. Twenty years of cloud security built the signal infrastructure. The decision layer was never built. &lt;code&gt;stave apply --observations ./your-snapshot/&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Every Cloud Security Tool Works. None of Them Are Sufficient. Here's the Precise Diagnosis.</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Mon, 13 Jul 2026 11:30:19 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/every-cloud-security-tool-works-none-of-them-are-sufficient-heres-the-precise-diagnosis-532o</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/every-cloud-security-tool-works-none-of-them-are-sufficient-heres-the-precise-diagnosis-532o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Our CSPM secures the cloud." "Our SIEM detects threats." "Our CNAPP protects cloud-native applications." "Our AI flags risks."&lt;/p&gt;

&lt;p&gt;Each claim is wrong by a specific, testable standard. There's a discipline from TRIZ that forces you to state what a tool DOES to its DIRECT OBJECT. No indirect effects. No consumer perceptions. No metaphors. Just: what does the tool DO to what it TOUCHES?&lt;/p&gt;

&lt;p&gt;Applied to six cloud security tool categories, the discipline reveals a structural pattern: &lt;strong&gt;every tool produces signals. None produces decisions.&lt;/strong&gt; Operators must currently infer decisions from accumulated signals. Breaches live in that inference gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discipline: state the direct action
&lt;/h2&gt;

&lt;p&gt;A function is the intended direct physical action of the tool on the object. State it as Tool—Verb—Object. Reject anything that describes an indirect effect, a consumer perception, or a non-physical concept.&lt;/p&gt;

&lt;p&gt;The classic example: a ship propeller. "Propeller drives ship" is wrong. The propeller doesn't touch the ship. "Propeller pushes water" is correct. The propeller directly acts on the water; the water moves the ship. If you optimize for "driving the ship" you optimize the wrong thing. If you optimize for "pushing water" you optimize the right thing.&lt;/p&gt;

&lt;p&gt;Cloud security descriptions fail the same test:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Common description (wrong)&lt;/th&gt;
&lt;th&gt;Why wrong&lt;/th&gt;
&lt;th&gt;Correct formulation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"CSPM secures the cloud posture"&lt;/td&gt;
&lt;td&gt;"Secures" is consumer-perceived&lt;/td&gt;
&lt;td&gt;CSPM rule engine matches API responses against rule database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"SIEM detects security incidents"&lt;/td&gt;
&lt;td&gt;"Detects" + "incident" are indirect&lt;/td&gt;
&lt;td&gt;SIEM correlation engine matches log events against detection signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"CNAPP protects cloud-native apps"&lt;/td&gt;
&lt;td&gt;"Protects" is consumer-perceived&lt;/td&gt;
&lt;td&gt;CNAPP aggregation engine collects and deduplicates findings from multiple scanners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"SOAR automates incident response"&lt;/td&gt;
&lt;td&gt;"Automates" is meta&lt;/td&gt;
&lt;td&gt;SOAR playbook executor runs scripted action sequences against detected events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Checkov scans IaC for misconfigurations"&lt;/td&gt;
&lt;td&gt;"Scans" + "misconfiguration" are technical-system labels&lt;/td&gt;
&lt;td&gt;IaC scanner tests parsed HCL/JSON against rule patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"ML detects anomalies"&lt;/td&gt;
&lt;td&gt;"Anomaly" is non-physical&lt;/td&gt;
&lt;td&gt;ML model scores events against a learned baseline distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six categories. Six correct formulations. Now the structural pattern becomes visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six tools, six verbs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Primary function (correct)&lt;/th&gt;
&lt;th&gt;Verb&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CSPM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rule engine &lt;strong&gt;matches&lt;/strong&gt; API responses against rule database&lt;/td&gt;
&lt;td&gt;Matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CNAPP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Aggregation engine &lt;strong&gt;collects&lt;/strong&gt; and deduplicates findings from scanners&lt;/td&gt;
&lt;td&gt;Aggregates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIEM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Correlation engine &lt;strong&gt;matches&lt;/strong&gt; log events against signatures&lt;/td&gt;
&lt;td&gt;Matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SOAR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Playbook executor &lt;strong&gt;runs&lt;/strong&gt; scripted sequences against detections&lt;/td&gt;
&lt;td&gt;Runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IaC scanner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parser + matcher &lt;strong&gt;tests&lt;/strong&gt; parsed IaC against rule patterns&lt;/td&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ML detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trained model &lt;strong&gt;scores&lt;/strong&gt; events against learned baseline&lt;/td&gt;
&lt;td&gt;Scores&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six verbs: &lt;strong&gt;matches, aggregates, matches, runs, tests, scores.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every verb is signal-producing. Each tool takes input and produces a signal — a finding, an alert, a score, a playbook result. None of the verbs is decision-producing. None produces a verdict that a machine can act on without human interpretation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cloud security industry has built six categories of signal-producing tools and zero categories of decision-producing tools.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The absent-function inventory
&lt;/h2&gt;

&lt;p&gt;For each tool, what's missing — functions that should exist but don't:&lt;/p&gt;

&lt;h3&gt;
  
  
  CSPM: matches, but only after deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Adequate:   Matches API responses against rules (within rule coverage)
✗ Absent:     Pre-deployment matching (CSPM runs AFTER state exists in the cloud)
✗ Absent:     Machine-verifiable deterministic verdict (findings require human interpretation)
✗ Absent:     Auditable rule basis (rule libraries are often vendor-proprietary)
✗ Harmful:    Alert fatigue (match volume exceeds operator capacity)
✗ Harmful:    Severity-score inflation (vendors exaggerate to justify visibility)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CNAPP: aggregates, but amplifies the noise
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Adequate:   Collects findings from multiple scanners
✗ Absent:     Pre-deployment prevention (inherits CSPM's post-hoc limitation)
✗ Absent:     Deterministic verdict (aggregated findings are MORE noisy, not less)
✗ Harmful:    Amplifies alert volume (aggregation increases noise without increasing signal)
✗ Harmful:    Hides scanner-specific quality (which scanner produced which finding?)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SIEM: matches events, but only after they happen
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Adequate:   Matches log events against signatures (within signature coverage)
✗ Absent:     Prevention (SIEM matches AFTER events occur; cannot prevent)
✗ Absent:     Machine-verifiable verdict per asset (produces alerts, not asset-level verdicts)
✗ Harmful:    Alert fatigue at production scale
✗ Harmful:    Cost growth (log-volume billing incentivizes reducing collection)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SOAR: runs playbooks, but amplifies upstream errors
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Adequate:   Runs playbooks (executor logic is mature)
✗ Absent:     Input quality control (cannot determine if the detection is correct)
✗ Absent:     Prevention (runs AFTER a detection; unsafe state already exists)
✗ Harmful:    Amplifies false positives (false detection → automated false response)
✗ Harmful:    False sense of automation (operators stop reviewing; detection failures become silent)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  IaC scanner: tests source, but not deployed state
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Adequate:   Tests parsed IaC against rules (within rule coverage)
✗ Absent:     Post-deployment evaluation (tests source; not deployed state)
✗ Absent:     Identity and environment context (source file lacks the live account state
              it deploys into — a Terraform module is safe in isolation but unsafe when
              composed with an existing FullAccess role it attaches to)
✗ Absent:     Detection of console/CLI changes (manual changes bypass IaC entirely)
✗ Absent:     Drift detection (source vs live state divergence is invisible)
✗ Harmful:    False security from "clean IaC" (clean source ≠ clean deployed state)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ML detection: scores events, but can't explain or enumerate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Adequate:   Scores events against baseline (inference is mechanical and fast)
✗ Absent:     Auditable verdict basis (high score doesn't explain WHY)
✗ Absent:     Determinism (same event scores differently after retraining)
✗ Absent:     Catalog-style enumeration (model cannot be inspected for "what counts as unsafe")
✗ Harmful:    Probabilistic verdicts for binary decisions (score thresholds are arbitrary)
✗ Harmful:    Model drift produces silent failure (stops catching things without anyone noticing)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The cross-tool pattern
&lt;/h2&gt;

&lt;p&gt;Line up the absent functions across all six tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Absent function&lt;/th&gt;
&lt;th&gt;CSPM&lt;/th&gt;
&lt;th&gt;CNAPP&lt;/th&gt;
&lt;th&gt;SIEM&lt;/th&gt;
&lt;th&gt;SOAR&lt;/th&gt;
&lt;th&gt;IaC&lt;/th&gt;
&lt;th&gt;ML&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pre-deployment evaluation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓*&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deterministic verdict&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auditable rule basis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;~&lt;/td&gt;
&lt;td&gt;~&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Catalog-style enumeration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~&lt;/td&gt;
&lt;td&gt;~&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployed-state evaluation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;~&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snapshot-based reasoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*IaC scanners are pre-deployment but source-only. They test the declared intent, not the actual state that will exist after apply. A source-only check cannot account for identity and environment context. The Terraform plan says "attach this policy to this role." Whether the resultant state is safe depends on what permissions that role already has, what other policies are attached, and what SCPs constrain the account — none of which exist in the source file. The snapshot (planned state composed with live state) is the only valid object for a pre-deployment security decision.&lt;/p&gt;

&lt;p&gt;The pattern: &lt;strong&gt;no tool provides all six functions.&lt;/strong&gt; Most tools provide one or two. The combination of all six tools still leaves gaps. Because the gaps are structural, not coverage-based. Adding a seventh tool that also produces signals doesn't fill the decision gap.&lt;/p&gt;

&lt;p&gt;The most striking column: &lt;strong&gt;deterministic verdict&lt;/strong&gt; is absent in all six categories. No tool in the cloud security market produces a deterministic, machine-verifiable, per-asset verdict that a pipeline can act on without human interpretation.&lt;/p&gt;

&lt;p&gt;The second most striking row: &lt;strong&gt;snapshot-based reasoning&lt;/strong&gt; is absent in all six. Every tool operates on streams, events, findings, or source code — not on coherent point-in-time snapshots of actual cloud state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harmful-function pattern
&lt;/h2&gt;

&lt;p&gt;Line up the harmful functions across all six tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harmful function&lt;/th&gt;
&lt;th&gt;Tools producing it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Alert/finding volume exceeds operator capacity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CSPM, CNAPP, SIEM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Severity-score inflation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CSPM, CNAPP, ML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vendor lock-in by rule/signature format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CSPM, CNAPP, SIEM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amplification of upstream errors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SOAR, CNAPP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;False security from clean source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IaC scanners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model drift / silent failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost growth with scale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SIEM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The harmful pattern compounds: CSPM produces inflated-severity findings → CNAPP aggregates them (amplifying volume) → SIEM correlates them (adding more alerts) → SOAR runs playbooks against the accumulated noise (amplifying false positives). Each tool's harmful function feeds the next tool's harmful function. The aggregate is worse than any individual tool's harmful contribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete picture: existing tools vs the missing function
&lt;/h2&gt;

&lt;p&gt;Combine the absent-useful inventory and the harmful inventory into one view. The rows are the properties the problem requires. The columns are every tool category — including the missing one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Required property&lt;/th&gt;
&lt;th&gt;CSPM&lt;/th&gt;
&lt;th&gt;CNAPP&lt;/th&gt;
&lt;th&gt;SIEM&lt;/th&gt;
&lt;th&gt;SOAR&lt;/th&gt;
&lt;th&gt;IaC Scanner&lt;/th&gt;
&lt;th&gt;ML Detection&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Configuration Verifier&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pre-deployment evaluation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗ Post-deploy&lt;/td&gt;
&lt;td&gt;✗ Post-deploy&lt;/td&gt;
&lt;td&gt;✗ Post-event&lt;/td&gt;
&lt;td&gt;✗ Post-detection&lt;/td&gt;
&lt;td&gt;⚠️ Source only&lt;/td&gt;
&lt;td&gt;✗ Post-event&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅ Planned + deployed snapshots&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deterministic verdict&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗ Findings&lt;/td&gt;
&lt;td&gt;✗ Aggregated findings&lt;/td&gt;
&lt;td&gt;✗ Alerts&lt;/td&gt;
&lt;td&gt;✗ Playbook results&lt;/td&gt;
&lt;td&gt;✗ Findings&lt;/td&gt;
&lt;td&gt;✗ Scores&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅ COMPLIANT / NON_COMPLIANT per asset&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auditable rule basis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗ Proprietary&lt;/td&gt;
&lt;td&gt;✗ Proprietary&lt;/td&gt;
&lt;td&gt;⚠️ Signatures visible&lt;/td&gt;
&lt;td&gt;⚠️ Playbooks visible&lt;/td&gt;
&lt;td&gt;✅ Open rules&lt;/td&gt;
&lt;td&gt;✗ Model opaque&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅ Open YAML catalog, every control readable&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Catalog-style enumeration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Rules exist but vendor-locked&lt;/td&gt;
&lt;td&gt;⚠️ Inherited&lt;/td&gt;
&lt;td&gt;✗ Signatures ≠ invariants&lt;/td&gt;
&lt;td&gt;✗ No catalog&lt;/td&gt;
&lt;td&gt;✅ Rules enumerate&lt;/td&gt;
&lt;td&gt;✗ Model can't enumerate&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅ 2,949 controls, each traceable to documented failure&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployed-state evaluation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Queries live APIs&lt;/td&gt;
&lt;td&gt;✅ Inherits CSPM&lt;/td&gt;
&lt;td&gt;✅ Reads live events&lt;/td&gt;
&lt;td&gt;⚠️ Acts on detections&lt;/td&gt;
&lt;td&gt;✗ Source only&lt;/td&gt;
&lt;td&gt;✅ Reads live events&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅ Snapshots of actual cloud state&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snapshot-based reasoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗ Stream/API polling&lt;/td&gt;
&lt;td&gt;✗ Stream/API polling&lt;/td&gt;
&lt;td&gt;✗ Event stream&lt;/td&gt;
&lt;td&gt;✗ Event-driven&lt;/td&gt;
&lt;td&gt;✗ Source files&lt;/td&gt;
&lt;td&gt;✗ Event stream&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅ Coherent point-in-time snapshots&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compound cross-asset reasoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗ Per-resource rules&lt;/td&gt;
&lt;td&gt;⚠️ Heuristic correlation&lt;/td&gt;
&lt;td&gt;⚠️ Event correlation&lt;/td&gt;
&lt;td&gt;✗ Per-playbook&lt;/td&gt;
&lt;td&gt;✗ Per-resource rules&lt;/td&gt;
&lt;td&gt;⚠️ Behavioral patterns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅ Compound chains across configuration graph&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Machine-consumable output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ API but interpretation needed&lt;/td&gt;
&lt;td&gt;⚠️ API but noisy&lt;/td&gt;
&lt;td&gt;⚠️ Alerts need triage&lt;/td&gt;
&lt;td&gt;⚠️ Actions need approval&lt;/td&gt;
&lt;td&gt;✅ Exit codes&lt;/td&gt;
&lt;td&gt;✗ Scores need thresholding&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅ Exit code 0/3, pipeline acts directly&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary function verb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Matches&lt;/td&gt;
&lt;td&gt;Aggregates&lt;/td&gt;
&lt;td&gt;Matches&lt;/td&gt;
&lt;td&gt;Runs&lt;/td&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;Scores&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Evaluates&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Verdict&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the table column by column. Every existing category has gaps (✗) in the rows that matter most. Read the last column: the Configuration Verifier fills every gap. Because the function analysis of existing tools diagnosed what was missing, and the missing function was designed to fill the diagnosis.&lt;/p&gt;

&lt;p&gt;Read the bottom two rows. Six tools with six different verbs — matches, aggregates, matches, runs, tests, scores — all producing signals. One tool with a different verb — evaluates — producing verdicts. The verb difference is the category difference.&lt;/p&gt;

&lt;p&gt;The most revealing row: &lt;strong&gt;deterministic verdict&lt;/strong&gt;. Six ✗ marks. Every existing tool produces something that requires human interpretation before a machine can act on it. The Configuration Verifier produces a verdict a pipeline reads as an exit code. No interpretation, triage or threshold. Binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis: signals vs decisions
&lt;/h2&gt;

&lt;p&gt;The structural diagnosis from the function analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What existing tools produce:    SIGNALS
    - findings for human interpretation
    - alerts for human triage
    - scores for human thresholding
    - playbook results for human verification

What the problem requires:      DECISIONS
    - deterministic per-asset verdicts
    - machine-readable pass/fail
    - auditable reasoning per verdict
    - pre-deployment evaluation against enumerated forbidden states
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap between signals and decisions is the gap between the existing toolchain and the problem it claims to solve. No amount of better signals closes the gap. Because signals and decisions are different categories. A faster alert is still an alert. A smarter score is still a score. A better finding is still a finding. None is a verdict.&lt;/p&gt;

&lt;p&gt;The gap requires a tool with a different primary function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Existing PF verbs:    matches, aggregates, matches, runs, tests, scores
                      (all signal-producing)

Missing PF verb:      EVALUATES — produces a deterministic categorical verdict per asset
                      by applying an enumerated catalog of forbidden states
                      to a snapshot of actual cloud state
                      (decision-producing)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The missing verb is qualitatively different from the existing verbs. It's not a better version of matching or scoring. It's a different action that produces a different output (verdict vs signal) from a different input (snapshot vs stream) using a different substrate (enumerated catalog vs learned baseline).&lt;/p&gt;

&lt;h2&gt;
  
  
  The design brief
&lt;/h2&gt;

&lt;p&gt;The function analysis produces the brief. The brief:&lt;/p&gt;

&lt;p&gt;A tool whose primary function fills the absent-useful inventory must:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Take snapshots as its object&lt;/strong&gt; — Coherent point-in-time captures of actual cloud state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Apply a catalog of forbidden states&lt;/strong&gt; — An enumerated, human-authored, auditable specification of what unsafe means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Produce a verdict&lt;/strong&gt; — A deterministic, categorical, per-asset decision: compliant or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Operate pre-deployment&lt;/strong&gt; — Evaluate proposed state before it reaches the cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Be deterministic&lt;/strong&gt; — Same inputs, same verdict, every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Be auditable&lt;/strong&gt; — the catalog is external, readable, and independently verifiable. An auditor can read the full enforcement scope without accessing vendor internals.&lt;/p&gt;

&lt;p&gt;Six properties. Each is the complement of a specific absent-useful function in the cross-tool inventory. Together they describe a tool with the primary function: &lt;strong&gt;the verdict engine evaluates a snapshot of cloud state against a catalog of forbidden states, producing a deterministic per-asset verdict.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That primary function doesn't exist in any of the six existing categories. It's a different verb (evaluates), operating on a different object (snapshot), using a different substrate (catalog), producing a different output (verdict). The function analysis tells us the gap is structural. The tool that fills it is a new category, not a better version of an existing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnostic for your own toolstack
&lt;/h2&gt;

&lt;p&gt;Apply the same discipline to your tools:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. For each tool, state the primary function in Tool—Verb—Object form.&lt;/strong&gt; Reject non-physical vocabulary. What does the tool do to what it touches?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Classify each function.&lt;/strong&gt; Adequate useful (works), inadequate useful (undershoots), absent useful (should exist, doesn't), harmful (works against you).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Look at the absent-useful column across all tools.&lt;/strong&gt; The pattern of what's missing across the toolstack is the structural gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Look at the harmful column across all tools.&lt;/strong&gt; Do the harmful functions compound? Does one tool's harmful output feed the next tool's harmful input?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. State the missing primary function.&lt;/strong&gt; What verb, operating on what object, producing what output, would fill the absent-useful inventory?&lt;/p&gt;

&lt;p&gt;The answer to step 5 is the design brief for what your toolstack needs next. Not "a better version of tool X". A tool with a different primary function that fills the gaps the existing tools collectively leave.&lt;/p&gt;

&lt;p&gt;Every cloud security tool works. Each tool's primary function is adequate within its coverage. The diagnosis is that they all produce signals where the problem demands decisions. The gap is structural. Filling it requires a different verb.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The missing primary function — the verdict engine evaluates a snapshot against a catalog of forbidden states, producing a deterministic per-asset verdict — is implemented in &lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;Stave&lt;/a&gt;, an open-source cloud configuration verifier. The verb the industry doesn't have: evaluates. The output the industry doesn't produce: verdicts. The input the industry doesn't use: snapshots. Try the missing function: &lt;code&gt;bash examples/demo-ai-security/run.sh&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>security</category>
      <category>architecture</category>
      <category>devops</category>
    </item>
    <item>
      <title>DORA Metrics Measure Delivery Health. What Measures Security Posture Health?</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Sun, 12 Jul 2026 12:17:36 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/dora-metrics-measure-delivery-health-what-measures-security-posture-health-34c7</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/dora-metrics-measure-delivery-health-what-measures-security-posture-health-34c7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Delivery teams have DORA. Four metrics — deployment frequency, lead time for changes, mean time to restore, change failure rate that predict whether a team is shipping well. Thoughtworks recently added a fifth: rework rate, measuring how much of the pipeline is consumed by fixing work previously considered complete.&lt;/p&gt;

&lt;p&gt;These metrics changed how delivery organizations operate. Because they're &lt;em&gt;leading&lt;/em&gt; indicators. They tell you the trajectory before the outcome arrives. A team with increasing lead times is heading for trouble. A team with rising rework rate is accumulating debt. You see it in the metrics before you see it in the incidents.&lt;/p&gt;

&lt;p&gt;Security teams have no equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What security teams measure today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Finding counts.&lt;/strong&gt; "We found 247 misconfigurations this quarter." More scanning produces more findings. A team that scans more frequently or adds a new tool sees the number go up which looks worse even if posture is improving. Finding counts measure scanning effort, not security health.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance percentages.&lt;/strong&gt; "We're 94% compliant with CIS Benchmarks." This measures the last audit, not the current trajectory. A team at 94% today might be at 87% next week if three Terraform changes introduced misconfigurations. The percentage is a snapshot, not a trend. It rewards breadth of coverage over depth. 94% across 200 checks sounds better than 100% across 50 checks, even if the 50 are the ones that matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incident counts.&lt;/strong&gt; "We had two security incidents this quarter." This is a trailing indicator. It measures failures that already happened. A team with zero incidents might have excellent posture or might have excellent luck. You can't tell. By the time the count goes up, the damage is done.&lt;/p&gt;

&lt;p&gt;None of these answer the question delivery teams answer with DORA: &lt;strong&gt;are we getting better, and how fast?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The mapping
&lt;/h2&gt;

&lt;p&gt;The five DORA metrics adapt directly to security posture. The definitions are concrete and measurable from evaluation data that already exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation frequency&lt;/strong&gt; (maps to deployment frequency): how often do you verify your posture? A team that evaluates weekly has a 7-day window where misconfigurations can accumulate undetected. A team that evaluates on every commit has minutes. The metric isn't "how often do you scan". It's how often you produce a timestamped, deterministic evaluation result that you can compare to the previous one. Frequency determines your maximum detection latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time to remediate&lt;/strong&gt; (maps to lead time for changes): the duration from when a finding first appears to when it disappears from the evaluation results. A finding that appears on Monday and disappears by Wednesday has a 2-day remediation time. A finding that appears in January and is still present in June has a 5-month remediation time. The metric tells you how long risk persists after detection. Unlike finding counts, it penalizes findings that sit unresolved rather than rewarding findings that get detected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mean time to remediate&lt;/strong&gt; (maps to MTTR): the average remediation time across all findings in a period. This is the posture equivalent of "how fast do we recover?" A team with a 3-day MTTR closes findings before they compound. A team with a 90-day MTTR is accumulating open findings faster than it resolves them. The trend matters more than the absolute number — MTTR going up means the team is falling behind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New findings per evaluation&lt;/strong&gt; (maps to change failure rate): of all the changes that happened between evaluations, how many introduced new security findings? If every Terraform apply produces three new findings, the development process is introducing risk at a steady rate. If the number is trending down, the team's practices — code review, pre-deployment checks, policy-as-code are catching problems earlier. This is the metric that connects delivery practices to security outcomes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recurrence rate&lt;/strong&gt; (maps to rework rate): findings that were remediated and then reappeared. A public bucket gets fixed. A later change re-exposes it. The finding returns. That's security rework — unplanned effort to re-fix a problem previously considered complete.&lt;/p&gt;

&lt;p&gt;This is the most important of the five. Thoughtworks added rework rate to DORA specifically because it catches the "considered complete but wasn't" pattern. In delivery, that's a feature that ships, breaks, and needs patching. In security, it's a finding that's remediated, silently reintroduced, and caught only when the next evaluation runs or worse, when an attacker finds it first.&lt;/p&gt;

&lt;p&gt;A rising recurrence rate is the early warning signal that remediation isn't sticking. Either the fixes are superficial (the bucket was made private but the Terraform code still declares it public, so the next apply reverts it), or the process has no feedback loop (the developer who introduced the finding wasn't told about it, so they repeat the pattern).&lt;/p&gt;

&lt;h2&gt;
  
  
  Computing the metrics
&lt;/h2&gt;

&lt;p&gt;The data is already in the evaluation results. Every finding has a first-seen timestamp and a last-seen timestamp. Every evaluation run has a capture time. Two consecutive evaluation results contain everything needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/sufield/stave/pkg/stave"&lt;/span&gt;

&lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stave&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"findings-2026-05-28.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stave&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"findings-2026-06-01.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stave&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// The five metrics, computed from the diff:&lt;/span&gt;

&lt;span class="c"&gt;// 1. Evaluation frequency&lt;/span&gt;
&lt;span class="n"&gt;daysBetween&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CapturedAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CapturedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hours&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;

&lt;span class="c"&gt;// 2-3. Remediation time (per finding and mean)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Removed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;remediationTime&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastSeen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstUnsafe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// accumulate for mean&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// 4. New findings per evaluation (change failure rate)&lt;/span&gt;
&lt;span class="n"&gt;newFindings&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Added&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;totalAssets&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalAssets&lt;/span&gt;
&lt;span class="n"&gt;changeFailureRate&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newFindings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totalAssets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// 5. Recurrence rate&lt;/span&gt;
&lt;span class="n"&gt;recurrenceRate&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recurred&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Findings&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No dashboard or CLI command. A Go program the customer writes, runs at whatever frequency they choose, and sends wherever they want — Slack, a retrospective, a CI gate, a spreadsheet, or nowhere at all.&lt;/p&gt;

&lt;p&gt;The library exposes the data: &lt;code&gt;LoadResult&lt;/code&gt;, &lt;code&gt;Diff&lt;/code&gt;, typed structs with timestamps. The customer decides what the numbers mean. A recurrence rate of 5% might be acceptable for one team and alarming for another. That's their judgment, not the tool's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a library, not a dashboard
&lt;/h2&gt;

&lt;p&gt;Thoughtworks' guidance on DORA metrics: "We recommend using these metrics for team reflection and learning rather than just building complex dashboards. Simple mechanisms, such as check-ins during retrospectives, are often more effective than overly detailed tracking tools."&lt;/p&gt;

&lt;p&gt;A CLI command with flags and formatters is a dashboard in disguise. It embeds opinions about what to show, how to format it, and what thresholds matter. The customer gets options but not ownership.&lt;/p&gt;

&lt;p&gt;A library function that returns typed data has no opinion. The customer writes a 20-line program that computes the metric they care about, at the frequency they choose, with the thresholds they set. If they want a Slack message when recurrence rate exceeds 10%, they write that. If they want a weekly email with remediation time trends, they write that. If they want a CI gate that fails the build when new findings exceed zero, they write that.&lt;/p&gt;

&lt;p&gt;The library provides facts. The customer provides interpretation. Same architecture as the evaluator itself and for the same reason: the tool that embeds its own interpretation is the tool the customer outgrows first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each metric tells a security team
&lt;/h2&gt;

&lt;p&gt;Used together, the five metrics tell a story that no individual metric captures:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"We're scanning more but not remediating faster."&lt;/strong&gt; Evaluation frequency is up, remediation time is flat or rising. The team is detecting more but closing at the same rate. The bottleneck is downstream of detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Remediation isn't sticking."&lt;/strong&gt; Recurrence rate is rising. Findings get fixed and come back. The fixes are superficial, or the development process reintroduces the same patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"New risk is outpacing remediation."&lt;/strong&gt; New findings per evaluation exceeds remediated findings per evaluation. The backlog is growing. The team is falling behind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"We're getting faster at fixing but slower at preventing."&lt;/strong&gt; MTTR is improving, but change failure rate is rising. The team is getting better at response and worse at prevention. The investment should shift from faster remediation to fewer introductions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Posture is improving."&lt;/strong&gt; All five trending in the right direction — evaluating more frequently, remediating faster, introducing fewer new findings, and seeing fewer recurrences. This is the signal that practices are working, not just tools.&lt;/p&gt;

&lt;p&gt;No single metric tells the story. A team with zero new findings but a 30% recurrence rate is on a treadmill. A team with high new findings but a 2-day MTTR and zero recurrences is catching and fixing fast. The five together are the leading indicator of posture health. The thing security teams have been missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-assisted development connection
&lt;/h2&gt;

&lt;p&gt;Thoughtworks flags rework rate specifically in the context of AI-assisted development: "If lead times don't decrease and deployment frequency doesn't increase, faster code generation doesn't translate into better outcomes. Conversely, degradation in stability metrics particularly rework rate provides an early warning sign of blind spots, technical debt and the risks of unchecked AI-assisted development."&lt;/p&gt;

&lt;p&gt;The same logic applies to AI-generated infrastructure. A Terraform module generated by an AI agent ships faster. If it introduces a security finding that gets remediated and then reappears when the module is reused elsewhere, that's AI-assisted security rework. The rework rate catches it. The finding count doesn't. It just goes up by one each time.&lt;/p&gt;

&lt;p&gt;Measuring generated infrastructure with posture DORA metrics answers the question that finding counts can't: is the AI making our cloud safer, or is it making our cloud bigger and equally vulnerable?&lt;/p&gt;

&lt;h2&gt;
  
  
  Action Items
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Produce two evaluation results.&lt;/strong&gt; Run your security evaluation tool today and again in a week. Save both outputs as JSON. You now have the raw data for all five metrics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Diff them.&lt;/strong&gt; Count: how many findings are new (added), how many were remediated (removed), how many came back (recurred), how many persisted (unchanged). Four numbers. That's the posture health summary for the week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Compute one metric.&lt;/strong&gt; Pick the one that matters most to your team right now. If you're drowning in findings, compute MTTR. It tells you whether you're closing faster or slower. If you recently adopted AI-assisted infrastructure, compute recurrence rate. It tells you whether fixes are sticking. If you're trying to justify investment in prevention, compute change failure rate. It tells you how many changes introduce new risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Bring it to a retrospective.&lt;/strong&gt; One number. One trend. "Our remediation time went from 12 days to 8 days this month" is a more useful conversation starter than "we have 247 findings." The number becomes a tool for reflection, not a dashboard to stare at.&lt;/p&gt;

&lt;p&gt;The delivery side of the industry figured this out years ago. The security side is still counting findings and calling it measurement. The same five metrics work. The data already exists. The question is whether you compute it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The library API for loading results and computing diffs is in &lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;Stave&lt;/a&gt; at &lt;code&gt;pkg/stave/&lt;/code&gt;. Tutorial 12 in the stave-guide covers building custom metrics programs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>cloud</category>
      <category>programming</category>
    </item>
    <item>
      <title>Context Engineering Optimizes the Input. Nobody's Checking the Output.</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Sat, 11 Jul 2026 11:15:41 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/context-engineering-optimizes-the-input-nobodys-checking-the-output-2i2</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/context-engineering-optimizes-the-input-nobodys-checking-the-output-2i2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The term context engineering has moved from optimization tactic to architectural discipline. Thoughtworks put it in their Technology Radar Vol. 34 (April 2026) under Adopt. The argument: as agents tackle complex tasks, dumping raw data into large context windows leads to degraded reasoning. The fix: treat the context window as a design surface, engineer what goes in.&lt;/p&gt;

&lt;p&gt;The techniques work. Progressive context disclosure starts with a lightweight index and pulls in only what's needed. Context graphs model institutional reasoning as structured, queryable data. Dynamic retrieval selects tools and loads only necessary servers. Stateful compression summarizes intermediate outputs to manage working memory in long-running workflows.&lt;/p&gt;

&lt;p&gt;All of these optimize the input. None of them verify the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asymmetry
&lt;/h2&gt;

&lt;p&gt;Every engineering discipline has two pipelines: one that prepares the input and one that verifies the output.&lt;/p&gt;

&lt;p&gt;Manufacturing has incoming material inspection (input quality) AND outgoing quality control (output verification). A factory that perfects its raw material sourcing but has no inspection on the finished product still ships defective goods — just goods made from better materials.&lt;/p&gt;

&lt;p&gt;Compilers have parsing and type-checking (input validation) AND the test suite and formal verification (output verification). A compiler that accepts well-formed source doesn't guarantee correct machine code. The type system catches structural errors. The test suite catches behavioral errors. Neither replaces the other.&lt;/p&gt;

&lt;p&gt;AI-assisted development has context engineering (input optimization) AND... nothing. The output goes to code review, where a human reads it and forms an opinion. There is no mechanical verification of the output against declared properties. The entire industry investment is on the input side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input pipeline (where the industry invests):
  RAG → context graphs → progressive disclosure →
  dynamic retrieval → stateful compression →
  optimized context window → LLM

Output pipeline (where the industry doesn't):
  LLM → generated code → ???
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ??? is a human reviewer. Sometimes. If they have time. If they understand the code. If they know the properties that should hold. If they notice the subtle violation buried in plausible-looking output that was generated from excellent context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better input doesn't guarantee correct output
&lt;/h2&gt;

&lt;p&gt;Four structural reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The LLM can ignore the context.&lt;/strong&gt; Liu et al. (2023) documented this as the "lost in the middle" problem: LLM performance degrades when relevant information sits in the middle of a long context window. Longpre et al. (2021) showed that when provided context conflicts with training data, the model frequently defaults to its training. Context engineering optimizes &lt;em&gt;what's provided&lt;/em&gt;. It cannot guarantee &lt;em&gt;what's used&lt;/em&gt;. The model makes a probabilistic selection from everything it sees — training weights and context combined. No amount of context engineering gives you a guarantee that a specific piece of context will influence a specific output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compression is lossy.&lt;/strong&gt; Stateful compression — summarizing intermediate outputs to manage context length is performed by another LLM. A non-deterministic system deciding what information to discard from its own working memory. The compression may drop the detail that matters for the next step. You can't know the detail in advance, because you can't predict what the downstream task will need. The compression optimizes token count. It doesn't optimize for preserving the information the next step requires, because that requires knowing the future.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context graphs model knowledge, not contracts.&lt;/strong&gt; A context graph that encodes "policy X has exception Y with precedent Z" tells the LLM about the rules. It doesn't check whether the LLM followed them. The graph is a reference document — structured, queryable, better than unstructured prose. But a reference document is not a verification contract. The LLM can read the graph, generate output that contradicts it, and nobody knows unless a human catches it during review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progressive disclosure assumes the agent knows what it needs.&lt;/strong&gt; The agent starts with a lightweight index and pulls in context it determines is relevant. That determination is made by the same non-deterministic system that will use the context to generate output. If the agent's judgment about what's relevant is wrong. If it doesn't pull in the security policy, the architectural constraint, the edge case documentation — the missing context produces a missing property in the output. The system optimized for signal-to-noise ratio. The optimization was performed by the system that has the noise problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output verification pipeline
&lt;/h2&gt;

&lt;p&gt;A verification pipeline checks the output against declared properties — mechanically, deterministically, on every change. The properties are contracts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Context (what the input pipeline provides):
  "Here's the API documentation for the payment service."
  "Here's the coding convention for error handling."
  "Here's the schema for the database tables."

Properties (what the output pipeline checks):
  "No API endpoint returns PII without authentication."
  "No database query uses string concatenation for parameters."
  "No function calls an external service without a timeout."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context tells the LLM &lt;em&gt;what to build&lt;/em&gt;. The properties verify &lt;em&gt;what was built&lt;/em&gt;. The context is probabilistic. The LLM may use it. The properties are deterministic. The output either satisfies them or it doesn't.&lt;/p&gt;

&lt;p&gt;Three levels of output verification, from most accessible to most rigorous:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Property-based testing.&lt;/strong&gt; One afternoon to adopt. Instead of testing five hand-written examples, test one property across 10,000 randomly generated inputs. "For all valid inputs, the output amount is never negative." QuickCheck, Hypothesis, and similar tools generate the inputs. The property is the contract. The test checks every generated case against it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contract testing.&lt;/strong&gt; The API specification (OpenAPI, JSON Schema, protobuf) is the contract. Every generated endpoint is checked against the spec — fields present, types correct, status codes valid. The spec exists in most codebases already. The enforcement is a CI check that fails the build when the generated code deviates from the declared contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Formal verification.&lt;/strong&gt; The property is proved for all possible inputs, not just tested against a sample. "No IAM principal in this account can reach administrator access through any permission path." The proof is mathematical. The verification is exhaustive. Tools like Z3, TLA+, and Alloy provide the solver. The property is the specification. The proof is the evidence.&lt;/p&gt;

&lt;p&gt;Each level catches things the previous level misses. Property-based testing catches edge cases example-based tests miss. Contract testing catches interface violations property tests don't cover. Formal verification catches mathematical properties no test can exhaust. All three verify the output. None of them depend on context quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The industry's blind spot
&lt;/h2&gt;

&lt;p&gt;Better context produces better output on average. The blind spot is treating it as &lt;em&gt;sufficient&lt;/em&gt; as the architectural solution to AI reliability rather than one half of it.&lt;/p&gt;

&lt;p&gt;The Thoughtworks framing: "Treating AI context as a static text box is a fast track to hallucinations." True. The complete statement: "Treating AI output as unverified is a fast track to confident-looking failures, regardless of how well you engineered the context."&lt;/p&gt;

&lt;p&gt;Context engineering makes the failures rarer. Output verification catches the failures that remain. The failures that survive good context are subtle where the context was excellent, the output looks correct, the review approves, and the violation hides in a composition or an edge case or a negative property that wasn't in any context graph.&lt;/p&gt;

&lt;p&gt;These are the failures that cause production incidents. Because the output was never checked against a contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asymmetry is solvable
&lt;/h2&gt;

&lt;p&gt;The input pipeline took years to mature, from raw prompting to RAG to context graphs to progressive disclosure. Each step was an engineering improvement that made the input better. The output pipeline is at step zero for most teams: no contracts, no mechanical verification, just human review.&lt;/p&gt;

&lt;p&gt;The tools exist. Property-based testing is decades old. Contract testing is standard in API development. Formal verification is used at AWS, Microsoft, and Intel for critical systems. The gap is the recognition that input optimization and output verification are independent concerns, both necessary, neither sufficient.&lt;/p&gt;

&lt;p&gt;A team that engineers its context pipeline and adds one property check per sprint — one mechanical verification of one declared property on every change is building both pipelines. The context pipeline reduces the frequency of failures. The verification pipeline catches the failures that survive. The combination provides reliability.&lt;/p&gt;

&lt;p&gt;Context engineering is ingredient sourcing. Output verification is quality inspection. The restaurant that does both ships fewer defective plates than the one that perfects its sourcing and hopes the chef never makes a mistake.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Liu, N.F., et al. (2023). "Lost in the Middle: How Language Models Use Long Contexts." &lt;em&gt;Transactions of the ACL&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Longpre, S., et al. (2021). "Entity-Based Knowledge Conflicts in Question Answering." &lt;em&gt;EMNLP 2021&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Thoughtworks (2026). "Technology Radar, Vol. 34." Context Engineering under Adopt.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>architecture</category>
      <category>engineering</category>
    </item>
    <item>
      <title>AWS Just Added OAuth to the MCP Server. It Silently Changed the Meaning of Your Existing IAM Policies.</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Fri, 10 Jul 2026 11:30:11 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/aws-just-added-oauth-to-the-mcp-server-it-silently-changed-the-meaning-of-your-existing-iam-408k</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/aws-just-added-oauth-to-the-mcp-server-it-silently-changed-the-meaning-of-your-existing-iam-408k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On July 9, 2026, AWS introduced OAuth support for the AWS MCP Server. The &lt;a href="https://aws.amazon.com/blogs/security/introducing-oauth-support-for-aws-mcp-server/" rel="noopener noreferrer"&gt;announcement&lt;/a&gt; describes a new action namespace (&lt;code&gt;signin:&lt;/code&gt;), new condition keys, and a new resource type. It shipped with a managed policy (&lt;code&gt;AWSMCPSignInOAuthAccessPolicy&lt;/code&gt;) ready to attach.&lt;/p&gt;

&lt;p&gt;The mechanism is straightforward: an agent authenticates to the AWS Sign-In token endpoint using SigV4 credentials. The same credentials an EC2 instance profile or Lambda execution role already has and receives a short-lived OAuth access token in exchange. The agent then uses that token to access the AWS MCP Server.&lt;/p&gt;

&lt;p&gt;The OAuth flow is well-designed. But it changes the IAM surface in ways that deserve immediate attention, because every IAM policy with &lt;code&gt;Action: "*"&lt;/code&gt; or &lt;code&gt;Action: "signin:*"&lt;/code&gt; now implicitly grants OAuth agent authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Actions
&lt;/h2&gt;

&lt;p&gt;Two actions control the OAuth flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;signin:AuthorizeOAuth2Access&lt;/code&gt; — interactive authorization code flow (browser-based)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signin:CreateOAuth2Token&lt;/code&gt; — token issuance (authorization code exchange, refresh, or client credentials)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more support token lifecycle management:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;signin:IntrospectOAuth2Token&lt;/code&gt; — check token status&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signin:RevokeOAuth2Token&lt;/code&gt; — revoke a token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four target a new resource type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arn:aws:signin:*:*:service-principal/aws-mcp.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Condition Keys That Matter
&lt;/h2&gt;

&lt;p&gt;AWS introduced two OAuth-specific condition keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;signin:OAuthRedirectUri&lt;/code&gt; — where the authorization response is delivered&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signin:OAuthGrantType&lt;/code&gt; — which OAuth flows are permitted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The blog post includes a restrictive policy example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"signin:AuthorizeOAuth2Access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"signin:CreateOAuth2Token"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:signin:*:*:service-principal/aws-mcp.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"StringLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"signin:OAuthRedirectUri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"signin:OAuthGrantType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"refresh_token"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the policy AWS recommends for local development. Notice what it restricts: redirect URI to localhost, grant types to interactive flows only.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Managed Policy Does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AWSMCPSignInOAuthAccessPolicy&lt;/code&gt; grants the two primary actions. AWS provides it so administrators can attach it without writing custom policy. The question is whether the identities it gets attached to are the right ones.&lt;/p&gt;

&lt;p&gt;An EC2 instance role with &lt;code&gt;AdministratorAccess&lt;/code&gt; that also has this policy attached can now issue OAuth tokens that carry its full permission scope. The role was already powerful. The OAuth token makes that power delegatable to an agent process that may not have the same operational controls the role was designed for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implicit Grant Problem
&lt;/h2&gt;

&lt;p&gt;Here is the IAM surface change that most organizations will miss.&lt;/p&gt;

&lt;p&gt;Before July 9, a policy with &lt;code&gt;Action: "*"&lt;/code&gt; granted every AWS action across every service. After July 9, that same policy also grants &lt;code&gt;signin:AuthorizeOAuth2Access&lt;/code&gt; and &lt;code&gt;signin:CreateOAuth2Token&lt;/code&gt;. The policy did not change. The set of actions it covers changed.&lt;/p&gt;

&lt;p&gt;The same applies to &lt;code&gt;Action: "signin:*"&lt;/code&gt;. Any policy that used the &lt;code&gt;signin:&lt;/code&gt; namespace for console-related actions now also grants OAuth agent authorization.&lt;/p&gt;

&lt;p&gt;These are not misconfigured policies. They are policies that were correct last week and have a different meaning this week. The identity that has them may not know it can now authorize agents to act on its behalf.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Defenders Should Check
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which identities have OAuth capability?&lt;/strong&gt; Audit for &lt;code&gt;signin:AuthorizeOAuth2Access&lt;/code&gt; and &lt;code&gt;signin:CreateOAuth2Token&lt;/code&gt; including grants via &lt;code&gt;signin:*&lt;/code&gt; and &lt;code&gt;*&lt;/code&gt;. The identities that have these permissions may not be the ones you want authorizing agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are redirect URIs constrained?&lt;/strong&gt; Without a &lt;code&gt;signin:OAuthRedirectUri&lt;/code&gt; condition, OAuth tokens can be delivered to arbitrary endpoints. This is a token theft vector regardless of which grant type is used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the resource scoped?&lt;/strong&gt; A policy with &lt;code&gt;Resource: "*"&lt;/code&gt; permits authorization against any service principal in the &lt;code&gt;signin:&lt;/code&gt; namespace — current and future. Scoping to &lt;code&gt;arn:aws:signin:*:*:service-principal/aws-mcp.amazonaws.com&lt;/code&gt; limits authorization to the MCP Server specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can anyone revoke tokens?&lt;/strong&gt; If OAuth tokens are being issued but no identity in the account has &lt;code&gt;signin:RevokeOAuth2Token&lt;/code&gt;, compromised tokens cannot be revoked. The same applies to &lt;code&gt;signin:IntrospectOAuth2Token&lt;/code&gt; — without it, defenders cannot inspect token status during incident response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the blast radius of delegation?&lt;/strong&gt; The real risk is not which grant type is used. &lt;code&gt;client_credentials&lt;/code&gt; is the intended grant for machine-to-machine flows — an EC2 instance converting SigV4 to OAuth is the documented use case. The risk is when the identity that can delegate has permissions far beyond what the agent task requires. An admin role issuing OAuth tokens creates a path where an agent operates with full admin scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CloudTrail Angle
&lt;/h2&gt;

&lt;p&gt;OAuth authorization events — &lt;code&gt;AuthorizeOAuth2Access&lt;/code&gt;, &lt;code&gt;CreateOAuth2Token&lt;/code&gt;, token revocations are recorded in CloudTrail. This is good for detection. It also means that if your CloudTrail destination bucket is compromised (the bucket hijacking vector), the OAuth audit trail goes with it.&lt;/p&gt;

&lt;p&gt;Organizations that already monitor CloudTrail for IAM credential use should extend their detection to cover the &lt;code&gt;signin:&lt;/code&gt; event source. Token issuance patterns — which identities are issuing tokens, how frequently, from which source IPs — are the new signal to baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Changes About IAM Reviews
&lt;/h2&gt;

&lt;p&gt;Every IAM review process that inventories dangerous actions needs to add the &lt;code&gt;signin:&lt;/code&gt; namespace. The standard lists (&lt;code&gt;iam:*&lt;/code&gt;, &lt;code&gt;s3:*&lt;/code&gt;, &lt;code&gt;kms:*&lt;/code&gt;, &lt;code&gt;sts:AssumeRole&lt;/code&gt;) now have a peer: &lt;code&gt;signin:CreateOAuth2Token&lt;/code&gt; is a credential-issuance action with the same architectural significance as &lt;code&gt;sts:AssumeRole&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The difference is that &lt;code&gt;sts:AssumeRole&lt;/code&gt; has been in every security team's mental model for years. &lt;code&gt;signin:CreateOAuth2Token&lt;/code&gt; was introduced this week. The policies that grant it already exist — they just didn't grant it until now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Detection with Stave
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;Stave&lt;/a&gt; is an open-source CLI that evaluates cloud configuration snapshots against a catalog of security controls. It ships with eight controls purpose-built for the OAuth MCP Server surface, covering each of the checks described above:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wildcard and implicit grants&lt;/strong&gt; — Detects identities that acquired OAuth capability through &lt;code&gt;Action: "*"&lt;/code&gt; or &lt;code&gt;Action: "signin:*"&lt;/code&gt; rather than explicit grants. These are the policies that changed meaning on July 9.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirect URI constraints&lt;/strong&gt; — Flags &lt;code&gt;signin:AuthorizeOAuth2Access&lt;/code&gt; grants that lack a &lt;code&gt;signin:OAuthRedirectUri&lt;/code&gt; condition, the token-delivery vector that matters regardless of grant type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource scoping&lt;/strong&gt; — Identifies OAuth grants with &lt;code&gt;Resource: "*"&lt;/code&gt; instead of the specific MCP service principal ARN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delegation blast radius&lt;/strong&gt; — Surfaces identities that combine &lt;code&gt;signin:CreateOAuth2Token&lt;/code&gt; with admin or elevated permissions — the over-privileged delegation path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token lifecycle gaps&lt;/strong&gt; — Flags accounts where tokens are being issued but no identity has &lt;code&gt;signin:RevokeOAuth2Token&lt;/code&gt; or &lt;code&gt;signin:IntrospectOAuth2Token&lt;/code&gt;, leaving defenders without revocation or inspection capability during incidents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed policy tracking&lt;/strong&gt; — Inventories &lt;code&gt;AWSMCPSignInOAuthAccessPolicy&lt;/code&gt; attachments so teams can review which identities received the AWS-provided OAuth grant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth inventory&lt;/strong&gt; — Surfaces every identity with any &lt;code&gt;signin:&lt;/code&gt; OAuth action, giving defenders a complete picture of which identities participate in OAuth delegation.&lt;/p&gt;

&lt;p&gt;Stave evaluates these controls against point-in-time snapshots of IAM configuration, so they can run in CI, on a schedule, or as a one-off audit. The controls are declarative YAML. Teams can inspect the detection logic, adjust thresholds, or extend the catalog for their own policies.&lt;/p&gt;

</description>
      <category>security</category>
      <category>aws</category>
      <category>cloud</category>
      <category>iam</category>
    </item>
    <item>
      <title>You Cannot Compose Safety From Individual Findings: The Formal Result the Cloud Security Industry Ignores</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Thu, 09 Jul 2026 11:17:12 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/you-cannot-compose-safety-from-individual-findings-the-formal-result-the-cloud-security-industry-kbk</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/you-cannot-compose-safety-from-individual-findings-the-formal-result-the-cloud-security-industry-kbk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every cloud security scanner on the market such as Prowler, ScoutSuite, Checkov, AWS Config Rules operates the same way. It evaluates individual resources against individual rules. S3 bucket: is public access blocked? IAM role: is the trust policy scoped? Security group: are ports restricted?&lt;/p&gt;

&lt;p&gt;Each finding is correct. Each resource passes or fails its check. And the security team reports: 847 findings, 12 critical, here's your posture score.&lt;/p&gt;

&lt;p&gt;The report is formally meaningless as a system-level safety claim.&lt;/p&gt;

&lt;p&gt;This is a theoretical impossibility. You cannot derive system-level safety from component-level findings alone. The result has been proven, reproven, and formalized across four decades of computer science. The cloud security industry has collectively ignored it.&lt;/p&gt;

&lt;p&gt;This article traces the formal lineage, shows why it matters practically, and describes what a verification framework that addresses system-level safety would require.&lt;/p&gt;




&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;The core claim: individual component safety proofs are &lt;em&gt;insufficient&lt;/em&gt; for system-level safety. You need explicit specifications of the interfaces between components — what each component assumes about its environment, and what it guarantees to its environment. Without those interface specifications, individual safety proofs tell you nothing about the composed system.&lt;/p&gt;

&lt;p&gt;This has been formally established across multiple independent lines of work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The lineage
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Owicki and Gries (1976)&lt;/strong&gt; got there first. Working on proofs of parallel programs, they showed that a process with a valid individual correctness proof could behave &lt;em&gt;incorrectly&lt;/em&gt; in composition because another process modified shared state that the first process's proof assumed was stable. They called this the non-interference problem.&lt;/p&gt;

&lt;p&gt;The practical translation: you can prove that process A is correct in isolation, prove that process B is correct in isolation, compose them, and get an incorrect system. The individual proofs are valid. The composition is broken. The proofs failed because they &lt;em&gt;didn't specify what each process assumed about its environment&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jones (1983)&lt;/strong&gt; provided the constructive solution: rely-guarantee reasoning. Each component specifies two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;rely condition&lt;/strong&gt;: what the component assumes its environment will or will not do&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;guarantee condition&lt;/strong&gt;: what the component promises to its environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Composition is valid if and only if each component's guarantee satisfies every other component's rely condition. If component A relies on shared state S being stable, and component B's guarantee includes modifying S, the composition is invalid — regardless of whether A and B individually pass their proofs.&lt;/p&gt;

&lt;p&gt;Jones didn't just prove that the problem exists. He proved what the solution requires: &lt;em&gt;explicit interface contracts between components&lt;/em&gt;. Without them, composition is unverified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lamport (1977)&lt;/strong&gt; formally defined the two categories of correctness properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Safety properties&lt;/strong&gt;: "nothing bad ever happens" — the system never enters a forbidden state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liveness properties&lt;/strong&gt;: "something good eventually happens" — every request eventually gets a response, every failure eventually triggers recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lamport proved that safety properties are closed under intersection. If two components each satisfy a safety property, the composed system satisfies the &lt;em&gt;intersection&lt;/em&gt; of those properties. But here's the critical subtlety: that intersection may be &lt;em&gt;weaker&lt;/em&gt; than the global safety property you need. The system-level property may require coordination between components that neither component's individual proof addresses. The intersection is formally valid and semantically vacuous — it proves the system is "safe" only because the assumptions rule out all real executions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abadi and Lamport (1993)&lt;/strong&gt;, in "Composing Specifications," formalized this precisely. They showed that composing specifications requires a &lt;em&gt;compatibility condition&lt;/em&gt; between the assumptions and guarantees of each component. Without proving compatibility, the composed specification may be satisfiable but only in degenerate cases that don't correspond to any real system behavior.&lt;/p&gt;

&lt;p&gt;The degenerate case has a name: &lt;em&gt;vacuous correctness&lt;/em&gt;. A composition is vacuously safe when the assumptions of the components are mutually exclusive — component A assumes the environment provides X, component B assumes the environment provides ¬X. Both components are individually correct. The composition is correct only because no real execution satisfies both assumptions simultaneously. In cloud terms: the system is secure because the network configuration prevents anyone from reaching the S3 bucket. But the bucket is also useless, because the network configuration prevents anyone from reaching it. The scanner reports zero findings. The system satisfies no business requirement. The safety proof is real and the safety is meaningless. Vacuous correctness is the result when you verify components without checking that their assumptions are mutually satisfiable.&lt;/p&gt;

&lt;p&gt;The result, consolidated: you cannot verify system safety by verifying components individually. You must also verify that the &lt;em&gt;interfaces&lt;/em&gt; between components — the rely-guarantee pairs, the assumption-guarantee contracts are mutually compatible. Any verification framework that evaluates only individual components is formally incapable of making system-level safety claims.&lt;/p&gt;




&lt;h2&gt;
  
  
  The cloud security application
&lt;/h2&gt;

&lt;p&gt;Consider two AWS resources, each individually configured according to security best practices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource A: an S3 bucket.&lt;/strong&gt; Its bucket policy grants &lt;code&gt;s3:GetObject&lt;/code&gt; to IAM role &lt;code&gt;role-X&lt;/code&gt;. A scanner evaluates this bucket and passes it: access is restricted to a specific role. No public access or wildcard principals. Clear finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource B: an IAM role.&lt;/strong&gt; &lt;code&gt;role-X&lt;/code&gt; has a trust policy allowing &lt;code&gt;sts:AssumeRole&lt;/code&gt; from any EC2 instance in the account via an instance profile. A scanner evaluates this role and passes it: the trust policy is scoped to a specific service principal (&lt;code&gt;ec2.amazonaws.com&lt;/code&gt;) within the account. No cross-account trust or wildcard. Clear finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The composition:&lt;/strong&gt; any EC2 instance in the account, including a compromised developer workstation, an unpatched test instance, or an attacker-controlled instance launched through a separate privilege escalation can assume &lt;code&gt;role-X&lt;/code&gt; and read every object in the bucket.&lt;/p&gt;

&lt;p&gt;The system is insecure. Both components passed their scans. The vulnerability exists only at the &lt;em&gt;edge&lt;/em&gt; — the relationship between the bucket policy and the role's trust boundary.&lt;/p&gt;

&lt;p&gt;In Jones's framework, the problem is precise: the S3 bucket's rely condition (implicit, unstated) is that &lt;code&gt;role-X&lt;/code&gt; is assumable only by authorized, trusted principals performing authorized operations. The IAM role's guarantee (any EC2 instance can assume it) violates that rely condition. The violation is invisible to any tool that evaluates components individually because the rely condition is &lt;em&gt;not a property of either component&lt;/em&gt;. It is a property of the interface between them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why node-level scanning is structurally insufficient
&lt;/h2&gt;

&lt;p&gt;Better rules cannot fix this gap. It is a structural limitation of the verification framework itself.&lt;/p&gt;

&lt;p&gt;A node-level scanner evaluates a function: &lt;code&gt;f(resource) → {pass, fail}&lt;/code&gt;. The input is a single resource configuration. The output is a finding. The scanner's formal power is limited to properties expressible as predicates over individual resource states.&lt;/p&gt;

&lt;p&gt;System-level safety properties are not predicates over individual resource states. They are predicates over &lt;em&gt;paths through the resource graph&lt;/em&gt; — sequences of edges that, taken together, create an access path, a data flow, or a trust chain that violates a system invariant.&lt;/p&gt;

&lt;p&gt;"No unauthorized principal can read objects in bucket B" is not a property of bucket B's configuration. It is a property of every path through the resource graph that terminates at bucket B. It depends on every IAM role that the bucket policy references, every trust policy on those roles, every policy attachment that grants permissions to principals that can assume those roles, and every resource policy on every intermediate resource in the chain.&lt;/p&gt;

&lt;p&gt;A scanner that evaluates bucket B's configuration cannot verify this property. It doesn't have the information. The property is not &lt;em&gt;in&lt;/em&gt; bucket B — it is &lt;em&gt;in the graph&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This was proved by Owicki and Gries in 1976 for parallel processes, what Jones formalized in 1983 for compositional verification, and what Abadi and Lamport generalized in 1993 for specification composition. The cloud security industry is rediscovering — slowly, expensively, breach by breach — a result that has been formally established for half a century.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Capital One proved empirically
&lt;/h2&gt;

&lt;p&gt;The 2019 Capital One breach was not a code bug. It was a compound misconfiguration. Four individually-reasonable configurations composed into an exploitable access path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A WAF with an SSRF vulnerability (a code bug, but the only code bug in the chain)&lt;/li&gt;
&lt;li&gt;An EC2 instance with access to the AWS metadata service (standard default at the time)&lt;/li&gt;
&lt;li&gt;An IAM role attached to that instance with overly broad S3 permissions (individually assessable, but assessed in isolation)&lt;/li&gt;
&lt;li&gt;S3 buckets containing sensitive data accessible to that role (individually compliant — access was restricted to a specific role)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A node-level scanner evaluating each of these four resources individually would pass three of them. The WAF SSRF might be caught by a code-level scanner (Chris Betz's framework). But the &lt;em&gt;compound misconfiguration&lt;/em&gt; — the path from SSRF through metadata service through IAM role to S3 bucket was invisible to any tool evaluating individual resources.&lt;/p&gt;

&lt;p&gt;In Jones's terms: the S3 bucket relied on its granting role being assumable only through authorized code paths. The IAM role guaranteed it was assumable by any code running on the EC2 instance. The metadata service guaranteed it would dispense temporary credentials to any process on the instance. Each component's guarantee violated the next component's rely condition. No individual scan could detect this because the violation existed only at the interfaces.&lt;/p&gt;

&lt;p&gt;Capital One's breach was an empirical proof of Owicki and Gries (1976). The industry treated it as an operational failure. It was a theoretical inevitability.&lt;/p&gt;




&lt;h2&gt;
  
  
  What compositional verification requires
&lt;/h2&gt;

&lt;p&gt;If node-level scanning is structurally insufficient, what is sufficient?&lt;/p&gt;

&lt;p&gt;Jones's rely-guarantee framework provides the answer, translated to cloud security:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Define system-level safety properties.&lt;/strong&gt; These are the invariants the system must satisfy — not properties of individual resources, but properties of the system as a whole. Examples: "No principal outside the production account can assume any role with access to customer data." "No network path exists from any public-facing resource to any database resource that does not traverse a WAF and an authentication layer." "No IAM policy grants &lt;code&gt;s3:*&lt;/code&gt; to any role assumable by a service principal."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Model the resource graph.&lt;/strong&gt; Resources are nodes. Relationships (policy references, trust chains, network routes, data flows) are edges. The graph is the artifact under verification, not the individual resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Encode rely-guarantee contracts for each resource.&lt;/strong&gt; For each resource, specify: what does this resource assume about the resources it depends on (rely condition), and what does this resource promise to the resources that depend on it (guarantee condition)? For an S3 bucket that grants access to a role, the rely condition is that the role's trust policy restricts assumption to authorized principals. For the role, the guarantee condition is the actual set of principals that can assume it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Verify compositional compatibility.&lt;/strong&gt; Check that every resource's guarantee satisfies every dependent resource's rely condition. Any violation is a compound misconfiguration — an edge-level vulnerability that no node-level scanner can detect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Verify system properties against the composed specification.&lt;/strong&gt; Using the verified composition, check whether the system-level safety properties (Step 1) hold. This is where formal reasoning engines — SAT solvers, SMT solvers (Z3), Datalog engines (Soufflé) become necessary. The state space is too large for exhaustive enumeration but structured enough for formal analysis.&lt;/p&gt;

&lt;p&gt;Formally, this step is a &lt;em&gt;refinement check&lt;/em&gt;: verifying that the low-level configuration (the implementation — the resource graph as deployed) refines the high-level safety property (the specification — the system-level invariants defined in Step 1). Refinement means every behavior permitted by the implementation is also permitted by the specification. If the implementation allows an access path that the specification forbids, the refinement check fails, and the system is provably insecure with respect to that property. Formal verification goes beyond testing: a test checks whether a &lt;em&gt;specific&lt;/em&gt; behavior occurs; a refinement check proves whether &lt;em&gt;any&lt;/em&gt; forbidden behavior is possible.&lt;/p&gt;

&lt;p&gt;This is compositional verification when applied to cloud security. It requires evaluating edges, not nodes. It requires a specification of what secure means at the system level, not just a catalog of individual resource checks. It requires reasoning engines capable of operating over the graph, not just over individual vertices.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the industry builds instead
&lt;/h2&gt;

&lt;p&gt;The industry builds node-level scanners and adds more rules.&lt;/p&gt;

&lt;p&gt;Every time a new breach reveals a compound misconfiguration, the response is to add a rule to the scanner. After Capital One, tools added checks for IMDSv2, for overly broad IAM policies, for specific known-dangerous policy patterns. Each rule catches the &lt;em&gt;specific&lt;/em&gt; compound path that the breach revealed. It does not catch the &lt;em&gt;class&lt;/em&gt; of compound misconfiguration.&lt;/p&gt;

&lt;p&gt;This is the equivalent of responding to Owicki and Gries by adding more test cases instead of adopting rely-guarantee reasoning. It patches the last breach. It does not verify the system against the next one.&lt;/p&gt;

&lt;p&gt;The result is a growing catalog of rules. Prowler has over 300, AWS Config has hundreds more — each of which evaluates an individual resource against an individual predicate. The catalog grows. The false positive rate grows. The coverage of system-level safety properties does not grow, because the verification framework is &lt;em&gt;structurally incapable&lt;/em&gt; of evaluating system-level properties.&lt;/p&gt;




&lt;h2&gt;
  
  
  The specification that is missing
&lt;/h2&gt;

&lt;p&gt;The deepest problem is the absence of a specification.&lt;/p&gt;

&lt;p&gt;Every cloud deployment is a system. Every system has safety properties it must satisfy. But almost no organization has written those properties down as formal, mechanically-checkable specifications of the system-level invariants that must hold.&lt;/p&gt;

&lt;p&gt;Without a specification, verification is impossible. You can scan. You can discover. You can report findings. But you cannot verify, because verification requires a specification to verify &lt;em&gt;against&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is the missing specification. A formal description of what secure means for this specific system, expressed as properties over the resource graph, verifiable by formal reasoning engines.&lt;/p&gt;

&lt;p&gt;The Owicki-Gries-Jones-Lamport lineage doesn't just prove that compositional verification is necessary. It proves what the inputs to compositional verification must be: component-level rely-guarantee contracts, a compatibility verification between those contracts, and system-level safety properties to verify against the composed specification.&lt;/p&gt;

&lt;p&gt;The industry has none of these. It has node-level rules, individual findings, and posture scores. It has taken a formally insufficient verification framework and built an entire market around it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this leads
&lt;/h2&gt;

&lt;p&gt;The formal result is clear: system-level security requires system-level verification. System-level verification requires a specification of system-level properties, a model of the resource graph, and formal reasoning over the composition.&lt;/p&gt;

&lt;p&gt;The theoretical foundations are 50 years old. The practical tools exist — SMT solvers, Datalog engines, model checkers. The artifact required for verification (the cloud configuration snapshot) is exportable from every major cloud provider as JSON.&lt;/p&gt;

&lt;p&gt;What the industry lacks is the &lt;em&gt;practice&lt;/em&gt; of defining system-level security properties and verifying them compositionally. Until that practice exists, every cloud security scanner on the market is formally incapable of answering the question it claims to answer: is this system secure?&lt;/p&gt;

&lt;p&gt;Individual findings are not system security. The math proved this in 1976. It's time the industry caught up.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cloud</category>
      <category>aws</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Minimum Cut is the Remediation Plan</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Wed, 08 Jul 2026 12:39:39 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/the-minimum-cut-is-the-remediation-plan-1o0e</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/the-minimum-cut-is-the-remediation-plan-1o0e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two Questions, One Graph
&lt;/h2&gt;

&lt;p&gt;Cloud security scanners answer: &lt;em&gt;does a path exist from the internet to the crown jewels?&lt;/em&gt; Run a reachability algorithm — BFS, DFS, or a Datalog transitive closure over the graph of permissions, network routes, and trust relationships. If a path exists, emit a finding. Scanner does it. It's graph traversal, and it's solved.&lt;/p&gt;

&lt;p&gt;The harder question that almost nobody answers is: &lt;em&gt;what is the smallest set of changes that eliminates all paths?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Finding paths is cheap. There might be twelve paths from the internet to a production database, through different combinations of public subnets, overpermissioned IAM roles, VPC peering connections, and misconfigured security groups. A scanner that finds all twelve produces twelve findings. The engineer reads twelve findings and has to figure out, manually, which three changes would eliminate all twelve paths simultaneously. That reasoning — which edges to cut, where cutting one edge collapses multiple paths, where the graph has structural bottlenecks is not done by a scanner.&lt;/p&gt;

&lt;p&gt;This is the minimum cut problem. Ford and Fulkerson solved it in 1956.&lt;/p&gt;

&lt;h2&gt;
  
  
  Max-Flow Min-Cut, Stripped Down
&lt;/h2&gt;

&lt;p&gt;The Max-Flow Min-Cut Theorem says: in a flow network with a source and a sink, the maximum flow from source to sink equals the total capacity of the minimum cut. The smallest set of edges when removed disconnects the source from the sink.&lt;/p&gt;

&lt;p&gt;In a network engineering context, source is the sender, sink is the receiver, edges are links with bandwidth capacity, and the theorem tells you the bottleneck throughput. In cloud security, the mapping is different and the theorem tells you something more useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source.&lt;/strong&gt; The untrusted origin. The public internet, a compromised employee workstation, a stolen credential used from an external account — wherever the attacker starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sink.&lt;/strong&gt; The crown jewels. An S3 bucket with PII, a production RDS database, a Secrets Manager secret, a KMS key that encrypts everything else. In a real environment there are hundreds or thousands of sensitive resources. The standard technique is the &lt;strong&gt;super-sink&lt;/strong&gt;: a virtual node connected to every crown jewel by an edge with infinite capacity. Computing the min-cut from source to super-sink produces the minimum set of changes that disconnects the attacker from the entire high-value asset class simultaneously, not just from one resource. This is a single computation, not one per target. Cutting the path to the super-sink solves the remediation for every sensitive resource at once. The same technique works on the source side: a super-source connected to every entry point (public IPs, external trust relationships, compromised credential origins) computes the min-cut across all attack origins simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edges.&lt;/strong&gt; The connections between resources that an attacker can traverse. A security group rule permitting ingress. An IAM policy granting &lt;code&gt;s3:GetObject&lt;/code&gt;. A VPC peering connection with an overly broad route. A trust policy allowing role assumption from an external account. An event source mapping that triggers a Lambda function. Each edge is a step in the attack path.&lt;/p&gt;

&lt;p&gt;The graph is directed. Edge direction matters. An IAM policy granting &lt;code&gt;s3:GetObject&lt;/code&gt; is a unidirectional edge: it lets the principal read from the bucket but says nothing about traffic flowing the other way. A security group ingress rule on port 443 is also unidirectional: it permits inbound connections but the response traffic flows on the established connection, not through a separate edge. Max-Flow Min-Cut works correctly on directed graphs, but the implementation requires careful direction modeling because some AWS constructs look bidirectional. VPC peering is the primary example: a peering connection permits traffic in both directions, but the route table entries that use it are directional (subnet A routes to subnet B's CIDR through the peering). Model VPC peering as two directed edges (one per direction), each governed by its own route table entry and security group. The peering connection is the physical link; the routes are the directed edges in the graph. Getting the direction wrong inflates the path count. The algorithm finds paths that traffic can't traverse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capacity.&lt;/strong&gt; In network engineering, capacity is measured in megabits per second. In cloud security, it's almost always binary. The permission exists or it doesn't. The security group rule allows the traffic or it doesn't. The trust policy permits assumption or it doesn't. A path is open (capacity 1) or blocked (capacity 0). &lt;/p&gt;

&lt;p&gt;The theorem says: the maximum flow of attack paths from the internet to the database equals the total capacity of the minimum cut. Since capacity is binary, this simplifies to: &lt;strong&gt;the number of edge-disjoint attack paths equals the minimum number of edges you need to remove to disconnect the attacker from the data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That minimum set of edges is the remediation plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Min-Cut Beats Path Enumeration
&lt;/h2&gt;

&lt;p&gt;A scanner that finds twelve paths gives you twelve findings. You read them and notice that paths 1, 4, 7, and 11 all traverse the same overpermissioned IAM role. Paths 2, 5, and 9 all pass through the same security group rule. Paths 3, 6, 8, 10, and 12 all use the same VPC peering connection. Three changes — scope the role, tighten the security group, remove the peering route — eliminate all twelve paths. But the scanner didn't tell you that. It told you twelve things are broken. You did the graph theory in your head.&lt;/p&gt;

&lt;p&gt;The min-cut algorithm does this automatically. It takes the full graph (every resource, every permission, every network route), sets the source (internet) and sink (database), and computes the minimum set of edges when removed disconnects them. The output isn't twelve paths. It's three edges. Those three edges are the remediation plan. The smallest set of changes that eliminates all reachability between the attacker and the target.&lt;/p&gt;

&lt;p&gt;This is also why path enumeration scales badly. A complex environment might have hundreds of paths between the internet and a sensitive data store. Enumerating them all is computationally expensive and produces an overwhelming list. The min-cut is small — often three to five edges — regardless of how many paths exist. The remediation plan stays manageable even when the attack surface is enormous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Menger's Theorem: The Binary Case
&lt;/h2&gt;

&lt;p&gt;Since cloud permissions are almost always binary (the edge exists or it doesn't, with no partial capacity), the operative theorem is often Menger's rather than Ford-Fulkerson. Menger's Theorem states: the maximum number of edge-disjoint paths between two nodes equals the minimum number of edges whose removal disconnects them.&lt;/p&gt;

&lt;p&gt;Edge-disjoint means the paths share no edges. If three edge-disjoint paths exist between the internet and the database, the minimum cut must include at least three edges — one from each disjoint path. Cutting two isn't enough because the third path survives intact.&lt;/p&gt;

&lt;p&gt;This gives you a lower bound on the remediation effort without computing the full min-cut. Count the edge-disjoint paths and you know the minimum number of changes required. If there are five disjoint paths, five is the floor. No clever combination of three changes can eliminate all five.&lt;/p&gt;

&lt;p&gt;For cloud security, Menger's Theorem answers: "how resilient is this attack surface?" A target reachable by one path is fragile — one fix eliminates access. A target reachable by five disjoint paths is robust (from the attacker's perspective). The defender needs at least five changes. The disjoint path count is a severity metric: higher disjoint count means more work to remediate and more ways for the attacker to maintain access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nodes Versus Edges
&lt;/h2&gt;

&lt;p&gt;Ford-Fulkerson computes an &lt;strong&gt;edge cut&lt;/strong&gt;: which connections should be severed? In cloud security, you're often looking for a &lt;strong&gt;vertex cut&lt;/strong&gt;: which &lt;em&gt;resource&lt;/em&gt; is the bottleneck?&lt;/p&gt;

&lt;p&gt;If a single EC2 instance is the only path between the public subnet and the private data subnet, removing that instance disconnects the graph. That's a vertex cut. If a single IAM role is the only identity that bridges two otherwise-separated permission domains, revoking that role disconnects them. Also a vertex cut.&lt;/p&gt;

&lt;p&gt;The two formulations are mathematically interchangeable. Any vertex cut problem converts to an edge cut problem by splitting each node into two nodes connected by a single edge. The capacity of that edge represents the cost of removing the resource. After splitting, Ford-Fulkerson computes the answer in edge-cut form, and you translate back to the vertex cut.&lt;/p&gt;

&lt;p&gt;In practice, this is the difference between "which security group rule should I remove?" (edge cut) and "which IAM role is the choke point?" (vertex cut). Both are useful. The edge cut tells you the specific remediation. The vertex cut tells you the structural bottleneck. The resource whose compromise would open the most paths.&lt;/p&gt;

&lt;p&gt;This is why per-resource scanners miss the compound risk. They evaluate each resource independently (each node in isolation) and can detect that a security group rule is too permissive or an IAM role has wildcard permissions. What they can't detect is that this specific security group rule is the choke point through which all twelve attack paths flow, or that this specific IAM role is the vertex whose removal disconnects the internet from the database. That's graph-level analysis, not node-level analysis. The scanner sees trees. The min-cut sees the forest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Weighted Version
&lt;/h2&gt;

&lt;p&gt;Binary capacity (exists or doesn't exist) is the clear case. Real environments are messier. Not all edges are equally easy to exploit or equally easy to remediate.&lt;/p&gt;

&lt;p&gt;Assigning weights to edges captures this. Two possible weight schemes, each answering a different question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exploitability weights (the attacker's perspective).&lt;/strong&gt; An unpatched critical CVE on a public-facing instance has a high-capacity edge — easy for the attacker to traverse. An MFA-protected, IP-restricted, session-limited login has a low-capacity edge — hard to traverse. The max-flow computation finds the path of least resistance through the graph. The min-cut identifies where the defender can impose the most friction on the attacker with the fewest changes. This answers: "where do I get the most security gain per unit of effort?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation cost weights (the defender's perspective).&lt;/strong&gt; Removing a VPC peering connection might break a production workflow — high remediation cost. Scoping an overpermissioned IAM role might be free — low cost. The min-cut with remediation cost weights finds the cheapest set of changes that disconnects the graph. This answers: "what is the least-disruptive remediation plan that eliminates all attack paths?"&lt;/p&gt;

&lt;p&gt;The two weight schemes can be combined. The min-cut with combined weights finds the remediation plan that maximizes security improvement per unit of operational disruption — the economically optimal defense.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Temporal Dimension
&lt;/h2&gt;

&lt;p&gt;Here's where the min-cut connects to temporal consistency, the subject of the previous article in this series.&lt;/p&gt;

&lt;p&gt;A min-cut computation takes a graph as input and produces a set of edges as output. The graph is constructed from a configuration snapshot — the IAM policies, security group rules, VPC routes, and trust relationships captured at a point in time. If the snapshot is temporally inconsistent — different parts captured at different times with configuration changes in between — the graph describes a system state that may never have existed.&lt;/p&gt;

&lt;p&gt;Run Ford-Fulkerson on an inconsistent graph and the results are unreliable in three ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ghost path.&lt;/strong&gt; The scanner sees an IAM permission captured at 14:00 and a security group rule captured at 14:07. Ford-Fulkerson finds a path through both. The min-cut suggests removing the security group rule. But the IAM permission was revoked at 14:04. It doesn't exist at the same time as the security group rule. The path never existed. The remediation targets a choke point for an attack surface that was never real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The phantom cut.&lt;/strong&gt; The min-cut identifies three edges to remove. One of them is a VPC route that was already removed at 14:05, between when the source-side and sink-side resources were captured. The remediation plan includes a change that's already been made. The engineer "fixes" something that's already fixed and misses the remaining exposure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The missing path.&lt;/strong&gt; A new IAM permission was added at 14:03, after the identity group was captured but before the network group was captured. The identity snapshot doesn't include it. The graph is missing an edge. Ford-Fulkerson computes a min-cut that doesn't account for the new path. The remediation plan has a hole. It addresses the paths the scanner saw but misses the path that appeared mid-collection.&lt;/p&gt;

&lt;p&gt;All three are consequences of computing over an inconsistent cut (in the Chandy-Lamport sense) of the distributed system. The configuration snapshot is a cut through the execution history of the cloud infrastructure. If the cut is inconsistent, the graph it produces is fictional, and the min-cut of a fictional graph is a fictional remediation plan.&lt;/p&gt;

&lt;p&gt;The fix is the same consistency check described in the Lamport article: verify that no relevant change event falls between the capture times of the snapshot's service groups. If the cut is consistent, the graph is real, and the min-cut is the optimal remediation plan. If the cut is inconsistent, the min-cut computation should be qualified: "this remediation plan is based on a temporally inconsistent graph — re-collect for a consistent analysis."&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Implies for Tooling
&lt;/h2&gt;

&lt;p&gt;A scanner that enumerates paths is doing BFS/DFS. Useful, but produces a finding list that doesn't aggregate into a remediation plan.&lt;/p&gt;

&lt;p&gt;A scanner that computes the min-cut is doing Ford-Fulkerson (or Edmonds-Karp, or push-relabel). It produces a remediation plan directly: the minimum set of changes that disconnects the attacker from the target. The plan is optimal by construction — no smaller set of changes achieves the same disconnection.&lt;/p&gt;

&lt;p&gt;A scanner that computes the min-cut &lt;em&gt;and&lt;/em&gt; verifies temporal consistency is doing Ford-Fulkerson over a provably consistent graph. The remediation plan is both optimal and trustworthy — it addresses attack paths that exist simultaneously, not paths assembled from observations at different moments.&lt;/p&gt;

&lt;p&gt;The computational cost is modest. Ford-Fulkerson on a graph with V vertices and E edges runs in O(VE²) for Edmonds-Karp. A cloud environment with 10,000 resources and 50,000 edges (permissions, routes, trust relationships) is well within reach. The min-cut computation adds seconds, not hours, to the evaluation.&lt;/p&gt;

&lt;p&gt;The implementation maps naturally to the fact-export architecture. The configuration snapshot produces a graph of resources (nodes) and permissions/routes/trust (edges). The graph is exported to an engine that computes reachability (Soufflé/Datalog for path enumeration) and min-cut (a flow algorithm). The engine returns both the paths (the findings) and the min-cut (the remediation plan). The temporal consistency layer verifies the graph before the computation begins and qualifies the results if verification fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Question, Not Twelve
&lt;/h2&gt;

&lt;p&gt;The scanner that finds twelve paths produces twelve findings. The engineer reads twelve findings, reasons about shared edges, identifies three choke points, and implements three changes. The reasoning the engineer did — manually, in their head, possibly incorrectly — is a min-cut computation.&lt;/p&gt;

&lt;p&gt;The scanner that computes the min-cut produces three changes. The engineer reads three changes and implements them. The reasoning happened in the algorithm, where it's provably correct, instead of in the engineer's head, where it's approximated.&lt;/p&gt;

&lt;p&gt;Twelve findings and the hope that the engineer will figure it out. Or three changes and the proof that they're sufficient. The math to get from the first to the second has been available since 1956. Ford and Fulkerson solved it. Menger characterized the lower bound. Chandy and Lamport told us how to verify the graph. The algorithms exist and they're efficient. The missing specification is  applying them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the second in a series on formal methods applied to cloud security. The first, &lt;a href="https://dev.to/bala_paranj_059d338e44e7e/your-scanner-doesnt-know-what-time-it-is-46on"&gt;"Your Scanner Doesn't Know What Time It Is,"&lt;/a&gt; covers temporal consistency of configuration snapshots. The tools and examples reference &lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;Stave&lt;/a&gt;, an open-source cloud security tool (Apache 2.0, early-stage) that exports structured facts for external reasoning engines. The graph theory is general — it applies to any scanner that evaluates reachability across a configuration graph, regardless of which cloud provider or tool you use.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>security</category>
      <category>graphtheory</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Your Scanner Doesn't Know What Time It Is</title>
      <dc:creator>Bala Paranj</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:01:00 +0000</pubDate>
      <link>https://dev.to/bala_paranj_059d338e44e7e/your-scanner-doesnt-know-what-time-it-is-46on</link>
      <guid>https://dev.to/bala_paranj_059d338e44e7e/your-scanner-doesnt-know-what-time-it-is-46on</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;✓ Human-authored analysis; AI used for formatting and proofreading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Every cloud security scanner that evaluates configuration snapshots has the same hidden assumption: the snapshot is current and internally consistent. Neither is guaranteed. Nobody checks.&lt;/p&gt;

&lt;p&gt;A scanner pulls the IAM role configuration, evaluates it, pulls the S3 bucket configuration, evaluates it, and then produces a compound finding: "this IAM role can access 47 S3 buckets." The finding treats the IAM snapshot and the S3 snapshot as though they describe the same moment in time. They don't. The IAM snapshot was captured at 14:00:03. The S3 snapshot was captured at 14:07:41. In that seven-minute window, someone might have changed the IAM policy, added a new bucket, or deleted the role entirely.&lt;/p&gt;

&lt;p&gt;The compound finding is asserting something about a system state that may never have existed at any single point in time. It's splicing two observations from different moments into a single claim and presenting it as fact. The scanner has no mechanism to detect this, warn about it, or qualify the finding's confidence. The user reads "this role can reach 47 buckets" and trusts it, not knowing the role's policy was updated four minutes into the collection window and the actual number is 3.&lt;/p&gt;

&lt;p&gt;This isn't a theoretical edge case. It's the normal operating condition. Cloud infrastructure changes continuously. Collection runs take minutes to hours. The larger the environment, the wider the window, and the more likely a change occurred inside it.&lt;/p&gt;

&lt;p&gt;Leslie Lamport solved the formal version of this problem in 1978.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lamport and Time
&lt;/h2&gt;

&lt;p&gt;Lamport's "Time, Clocks, and the Ordering of Events in a Distributed System" is about a deceptively simple question: in a system where processes communicate by messages, what does it mean to say one event happened before another?&lt;/p&gt;

&lt;p&gt;His answer: physical clock time doesn't determine ordering. &lt;em&gt;Causality&lt;/em&gt; does. Event A happened before event B if A could have influenced B — either because A occurred earlier in the same process, or because A sent a message that B received. If neither condition holds, the events are concurrent: they happened independently, with no causal link.&lt;/p&gt;

&lt;p&gt;This creates a partial ordering over events. Some events are definitively before or after each other. Others are not comparable. They happened in parallel, and no amount of clock synchronization changes that.&lt;/p&gt;

&lt;p&gt;The insight that matters for configuration snapshots: &lt;strong&gt;two observations are consistent only if no event that would change the observed state occurred between them.&lt;/strong&gt; This is a statement about causal ordering, not wall-clock proximity.&lt;/p&gt;

&lt;p&gt;Two snapshots captured four hours apart with no intervening changes are perfectly consistent. Two snapshots captured thirty seconds apart with a policy change between them are not. The clock gap is irrelevant. The causal relationship between the observations and the change events determine consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Variables in Every Snapshot
&lt;/h2&gt;

&lt;p&gt;A configuration snapshot captures the state of a distributed system at a point in time. Three temporal variables govern whether findings derived from that snapshot are trustworthy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capture time.&lt;/strong&gt; When the scanner observed the configuration. This is a physical clock reading from the machine running the collection. It tells you when the observation happened but not whether the observation is still current.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation time.&lt;/strong&gt; When the scanner produces findings from the snapshot. This might be seconds after capture (continuous pipeline) or weeks later (M&amp;amp;A diligence on archived snapshots). The gap between capture time and evaluation time is the &lt;em&gt;freshness&lt;/em&gt; of the snapshot — how likely it is that the configuration has changed since observation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change time.&lt;/strong&gt; When the infrastructure was modified. This is the variable the scanner cannot directly observe. Snapshots capture state, not transitions. The scanner sees the configuration as it was at the moment of capture but not when it was last changed or what it was before. Change time lives in the audit log (CloudTrail, in AWS), not in the configuration snapshot.&lt;/p&gt;

&lt;p&gt;Most scanners ignore all three. They treat the snapshot as though it's always current (no freshness check) and always consistent (no cross-observation validation). The result is findings that present stale or inconsistent data with the same confidence as findings on fresh, consistent data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freshness: The First-Order Problem
&lt;/h2&gt;

&lt;p&gt;The simplest temporal problem is freshness: is the snapshot still current?&lt;/p&gt;

&lt;p&gt;A finding of FAIL on a three-week-old snapshot might already be remediated. A finding of PASS might no longer hold. Both are presented to the user with the same confidence as findings on a five-minute-old snapshot. The scanner reports certainty it doesn't have.&lt;/p&gt;

&lt;p&gt;The fix is a third verdict: &lt;strong&gt;UNCERTAIN&lt;/strong&gt;. When the snapshot's age exceeds a configurable threshold, findings are marked UNCERTAIN — not PASS, not FAIL, but "we ran the check and can't vouch for the answer because the data is old."&lt;/p&gt;

&lt;p&gt;This is straightforward engineering. Compare &lt;code&gt;evaluation_time - capture_time&lt;/code&gt; against a threshold. If the age exceeds the threshold, qualify the finding. The evaluation still runs (the underlying PASS or FAIL is preserved as metadata), but the verdict carries an epistemic qualifier: this assertion was true at capture time, but the data may no longer reflect the current state.&lt;/p&gt;

&lt;p&gt;Two implementation details matter:&lt;/p&gt;

&lt;p&gt;First, the evaluation time must be overridable. A breach reconstruction team evaluating a January snapshot in June needs the findings to be PASS or FAIL relative to January, not UNCERTAIN relative to June. The &lt;code&gt;--now&lt;/code&gt; flag sets the evaluation time to the moment of interest, making freshness relative to the scenario rather than the wall clock. Same snapshot, same controls, deterministic freshness verdicts — reproducibility preserved.&lt;/p&gt;

&lt;p&gt;Second, different services change at different rates. IAM policies might be stable for weeks. Security groups might change daily. EC2 instances come and go hourly. A single global freshness threshold is a blunt instrument. Per-service-group thresholds align the uncertainty qualifier with the change velocity of the data.&lt;/p&gt;

&lt;p&gt;Freshness is a probabilistic hedge. It says: "the data is old enough that changes are plausible, so we can't be confident." It doesn't say whether a change actually happened. For that, you need the causal ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consistency: The Lamport Problem
&lt;/h2&gt;

&lt;p&gt;Freshness handles the gap between capture and evaluation. Consistency handles the gap &lt;em&gt;within&lt;/em&gt; the capture itself.&lt;/p&gt;

&lt;p&gt;A scanner that collects by service group — identity first, then storage, then compute — produces snapshots with different capture times. The identity group might be captured at T1 and the storage group at T2, minutes or hours later. A compound finding that joins facts from both groups — "this IAM role can access these S3 buckets" is valid only if the configuration was stable between T1 and T2.&lt;/p&gt;

&lt;p&gt;This is Lamport's problem applied to snapshot collection. Each service group capture is an event. Each configuration change is an event. The compound finding is valid only when the captures are &lt;em&gt;causally after&lt;/em&gt; all relevant changes.&lt;/p&gt;

&lt;p&gt;Four scenarios:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Timeline 1: Consistent — no changes in window
  T1 (IAM captured)────────────T2 (S3 captured)
  No IAM or S3 changes between T1 and T2.
  → Compound finding is valid.

Timeline 2: Consistent — changes before window
  change──T1 (IAM captured)────T2 (S3 captured)
  The change happened before both captures.
  Both observations reflect post-change state.
  → Compound finding is valid.

Timeline 3: Inconsistent — change inside window
  T1 (IAM captured)──change──T2 (S3 captured)
  IAM was captured before the change.
  S3 was captured after the change.
  The compound finding joins pre-change IAM with post-change S3.
  → Compound finding may describe a state that never existed.

Timeline 4: Unknown — no change data
  T1 (IAM captured)────────T2 (S3 captured)
  No audit trail available. Cannot determine if
  changes occurred in the window.
  → Compound finding consistency is unknown.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timeline 3 is the dangerous case. The finding is not wrong in the way a false positive is wrong. It's wrong in a deeper way: it's asserting something about a composite system state that was never actual. The IAM permissions it describes existed at T1. The S3 bucket inventory it describes existed at T2. But the combination — these permissions on these buckets may never have been simultaneously true.&lt;/p&gt;

&lt;p&gt;Distributed systems theory has a formal term for what the scanner is attempting: a &lt;strong&gt;consistent cut&lt;/strong&gt;. A cut is a set of events, one per process, that represents a snapshot of the system's state. A cut is consistent if it respects causality — no observation in the cut reflects a state that was changed by an event the cut doesn't include. Timelines 1 and 2 are consistent cuts: the captures include the effects of all prior changes. Timeline 3 is an inconsistent cut: the IAM capture reflects the pre-change state while the S3 capture reflects the post-change state, and the change event falls across the cut boundary. Chandy and Lamport's 1985 snapshot algorithm solves this for systems where the observer can inject markers into the communication channels. A configuration scanner can't inject markers into AWS. It can only observe API responses after the fact. But the audit trail serves the same role: it records the events between observations, which is enough to determine after collection whether the cut was consistent.&lt;/p&gt;

&lt;p&gt;Timeline 4 is the common case. Without change data, the scanner can't distinguish Timelines 1, 2, and 3. The freshness threshold handles this probabilistically: if the window is small enough, a mid-window change is unlikely, so the finding is probably consistent. But probably is not provably.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lamport-Informed Consistency Check
&lt;/h2&gt;

&lt;p&gt;If the snapshot includes change data — the audit trail showing when each resource was last modified — the scanner can determine consistency formally rather than probabilistically.&lt;/p&gt;

&lt;p&gt;The algorithm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For each compound finding, identify the service groups whose facts contribute to the finding (e.g., identity and storage for a role-to-bucket blast radius).&lt;/li&gt;
&lt;li&gt;For each contributing service group, record its capture time (T1 for identity, T2 for storage).&lt;/li&gt;
&lt;li&gt;For each resource referenced in the finding, look up the most recent change event from the audit trail. Critically, "relevant change events" include not only direct changes to the resources named in the finding but also changes to the relationships and policies that govern them. A change to a Service Control Policy affects every IAM role in every account under that OU. A change to an S3 bucket policy affects every principal that accesses that bucket. A change to a VPC endpoint policy affects every service call routed through that endpoint. The set of relevant changes is the transitive closure of everything that could alter the finding's conclusion — not just the resources at the endpoints of the graph, but the edges and governing policies between them.&lt;/li&gt;
&lt;li&gt;For each pair of contributing groups, check whether any relevant change event (direct or relational) falls between their capture times.&lt;/li&gt;
&lt;li&gt;If no relevant change falls in any window: the finding is &lt;strong&gt;causally consistent&lt;/strong&gt;. The captures are after all relevant changes, regardless of wall-clock gap.&lt;/li&gt;
&lt;li&gt;If a relevant change falls between T1 and T2: the finding is &lt;strong&gt;causally inconsistent&lt;/strong&gt;. One group was captured before the change and one after. The finding may describe a state that never existed.&lt;/li&gt;
&lt;li&gt;If change data is unavailable for a resource: the finding's consistency is &lt;strong&gt;unknown&lt;/strong&gt;, and the freshness threshold applies as a probabilistic fallback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the vector clock applied to snapshot consistency. Each service group's capture is a vector entry. Each configuration change is an event. The compound finding is consistent when the capture vector is causally after the change vector for every resource in the finding.&lt;/p&gt;

&lt;p&gt;The formal property: a compound finding F over resources R1...Rn is consistent if and only if for every pair of resources Ri, Rj in F, there exists no change event E affecting Ri, Rj, or any governing policy G in the dependency chain between them (SCPs, RCPs, VPC endpoint policies, bucket policies), such that &lt;code&gt;capture_time(group(Ri)) &amp;lt; time(E) &amp;lt; capture_time(group(Rj))&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is decidable, computable from the audit trail, and produces a provable consistency verdict not a probabilistic one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Three concrete scenarios where the distinction between probabilistic freshness and formal consistency changes what you'd do:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M&amp;amp;A diligence.&lt;/strong&gt; You're evaluating a target company's infrastructure from snapshots they provided. The snapshots were collected over a three-day window. Your compound findings (privilege escalation paths, data exfiltration chains) join data from snapshots captured days apart. Without the consistency check, you're presenting findings about a system state that may never have existed simultaneously. With it, you know which findings are causally consistent (trustworthy) and which are not (need re-collection or qualification in the report).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breach reconstruction.&lt;/strong&gt; The IR team evaluates pre-breach snapshots to determine what the attacker could have reached. The snapshots were collected over several hours before the breach was detected. A compound finding that joins IAM data captured three hours before the breach with S3 data captured one hour before the breach is valid only if no IAM changes occurred in that two-hour window. CloudTrail can answer this definitively. If the attacker modified IAM policies during the window, the compound finding is joining pre-compromise IAM with post-compromise S3 — a misleading reconstruction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous governance with stale pipelines.&lt;/strong&gt; The collection pipeline for one service group fails silently. Identity data is fresh (captured tonight). Storage data is stale (last captured three days ago because the S3 collection job has been failing). The compound finding joins today's IAM permissions with three-day-old bucket inventory. Freshness catches this at the group level (the storage group is stale). Consistency catches the compound implication: any cross-group finding involving storage is uncertain, even if the identity findings alone are reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Layers of Temporal Reasoning
&lt;/h2&gt;

&lt;p&gt;The full temporal model has two layers, and they're complementary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Freshness.&lt;/strong&gt; Compares capture time against evaluation time. Answers: "is this data old enough that changes are plausible?" Produces UNCERTAIN when the age exceeds a threshold. This is the pragmatic layer. It requires no external data (just the timestamps on the snapshot) and catches the gross staleness case: the pipeline is broken, the snapshot is three weeks old, don't trust the findings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Consistency (Lamport-informed).&lt;/strong&gt; Compares capture times of different service groups against the change events from the audit trail. Answers: "did any relevant change occur between the captures that contribute to this compound finding?" Produces a provable consistency verdict. This is the formal layer. It requires the audit trail alongside the configuration snapshot.&lt;/p&gt;

&lt;p&gt;Most scanners have neither. Having Layer 1 alone is a significant improvement. It catches stale data and qualifies findings accordingly. Having both layers gives you a temporal reasoning framework that can prove whether a compound finding describes a state that actually existed. That's the gap between a scanner that reports findings and a scanner that reasons about the epistemic status of its own assertions.&lt;/p&gt;

&lt;p&gt;Lamport's contribution was a precise characterization of what it means to know the order of events when you can't share a clock. Configuration snapshots are observations made by a process (the scanner) about a distributed system (the cloud) where events (configuration changes) happen concurrently and independently. The formal framework exists. The question is whether the scanner uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Implies
&lt;/h2&gt;

&lt;p&gt;Layer 1 is buildable today with no external dependencies. Add &lt;code&gt;captured_at&lt;/code&gt; metadata to snapshots, compare against evaluation time, emit UNCERTAIN when stale. Every scanner should do this. Almost none do.&lt;/p&gt;

&lt;p&gt;Layer 2 is buildable when the audit trail is available. In AWS, CloudTrail records every configuration change with a timestamp. The data exists. Joining it with the snapshot's capture times is engineering, not research.&lt;/p&gt;

&lt;p&gt;Together, the two layers give you a scanner that can answer three questions no other scanner answers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is this finding based on current data? (Freshness)&lt;/li&gt;
&lt;li&gt;Is this compound finding based on consistent data? (Lamport consistency)&lt;/li&gt;
&lt;li&gt;If neither, what specifically is uncertain and why? (Qualified verdicts with named uncertainty)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The formal-methods grounding makes the UNCERTAIN verdict meaningful. "The data is old" is an observation. "This compound finding joins pre-change IAM state with post-change S3 state, and therefore describes a system configuration that may never have existed" is a proof. The difference is whether the user knows &lt;em&gt;why&lt;/em&gt; they shouldn't trust the finding, not just that they shouldn't.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The temporal reasoning framework described here is implemented in &lt;a href="https://github.com/sufield/stave" rel="noopener noreferrer"&gt;Stave&lt;/a&gt;, an open-source cloud security tool (Apache 2.0, early-stage). Layer 1 (freshness / UNCERTAIN verdict) is designed. Layer 2 (Lamport-informed consistency) is specified but not yet implemented — it requires audit trail integration alongside configuration snapshots. If you're building a scanner that evaluates configuration snapshots of distributed systems, Lamport's 1978 paper is the starting point. The problem it solves isn't academic. It's the temporal foundation that every compound finding depends on and almost no scanner verifies.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>security</category>
      <category>distributedsystems</category>
      <category>computerscience</category>
    </item>
  </channel>
</rss>
