agent-blast-radius 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. agent_blast_radius-0.1.0/.github/workflows/ci.yml +59 -0
  2. agent_blast_radius-0.1.0/.github/workflows/release.yml +50 -0
  3. agent_blast_radius-0.1.0/.gitignore +40 -0
  4. agent_blast_radius-0.1.0/CHANGELOG.md +23 -0
  5. agent_blast_radius-0.1.0/LICENSE +21 -0
  6. agent_blast_radius-0.1.0/Makefile +16 -0
  7. agent_blast_radius-0.1.0/PKG-INFO +300 -0
  8. agent_blast_radius-0.1.0/README.md +253 -0
  9. agent_blast_radius-0.1.0/SECURITY.md +23 -0
  10. agent_blast_radius-0.1.0/agent-blast-radius-analyzer-plan.md +298 -0
  11. agent_blast_radius-0.1.0/docs/divergences.md +25 -0
  12. agent_blast_radius-0.1.0/docs/headline.md +17 -0
  13. agent_blast_radius-0.1.0/docs/post.md +149 -0
  14. agent_blast_radius-0.1.0/docs/q2-shared-roles.md +54 -0
  15. agent_blast_radius-0.1.0/docs/roadmap.md +45 -0
  16. agent_blast_radius-0.1.0/docs/rule-semantics.md +86 -0
  17. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/README.md +27 -0
  18. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/agent.from-sources.yaml +40 -0
  19. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/agent.yaml +142 -0
  20. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/bedrock-action-group.json +33 -0
  21. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/mcp-tools.json +42 -0
  22. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/plan.json +1 -0
  23. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/.terraform.lock.hcl +25 -0
  24. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/README.md +23 -0
  25. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/crown_jewel.tf +5 -0
  26. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/outputs.tf +11 -0
  27. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/roles.tf +123 -0
  28. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/stub.zip +0 -0
  29. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/tools.tf +61 -0
  30. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/trust.tf +34 -0
  31. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/variables.tf +15 -0
  32. agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/versions.tf +21 -0
  33. agent_blast_radius-0.1.0/pyproject.toml +54 -0
  34. agent_blast_radius-0.1.0/scripts/build_action_dataset.py +162 -0
  35. agent_blast_radius-0.1.0/src/agent_blast_radius/__init__.py +10 -0
  36. agent_blast_radius-0.1.0/src/agent_blast_radius/analyze.py +20 -0
  37. agent_blast_radius-0.1.0/src/agent_blast_radius/ci.py +130 -0
  38. agent_blast_radius-0.1.0/src/agent_blast_radius/cli.py +146 -0
  39. agent_blast_radius-0.1.0/src/agent_blast_radius/data/VERSION +6 -0
  40. agent_blast_radius-0.1.0/src/agent_blast_radius/data/actions.json.gz +0 -0
  41. agent_blast_radius-0.1.0/src/agent_blast_radius/data/managed_policies.json.gz +0 -0
  42. agent_blast_radius-0.1.0/src/agent_blast_radius/data/rules/escalation.yaml +221 -0
  43. agent_blast_radius-0.1.0/src/agent_blast_radius/errors.py +34 -0
  44. agent_blast_radius-0.1.0/src/agent_blast_radius/iam/__init__.py +1 -0
  45. agent_blast_radius-0.1.0/src/agent_blast_radius/iam/actions.py +102 -0
  46. agent_blast_radius-0.1.0/src/agent_blast_radius/iam/arn.py +115 -0
  47. agent_blast_radius-0.1.0/src/agent_blast_radius/iam/conditions.py +77 -0
  48. agent_blast_radius-0.1.0/src/agent_blast_radius/iam/managed.py +35 -0
  49. agent_blast_radius-0.1.0/src/agent_blast_radius/iam/resolver.py +181 -0
  50. agent_blast_radius-0.1.0/src/agent_blast_radius/ir.py +345 -0
  51. agent_blast_radius-0.1.0/src/agent_blast_radius/loaders.py +163 -0
  52. agent_blast_radius-0.1.0/src/agent_blast_radius/parsers/__init__.py +34 -0
  53. agent_blast_radius-0.1.0/src/agent_blast_radius/parsers/bedrock.py +81 -0
  54. agent_blast_radius-0.1.0/src/agent_blast_radius/parsers/mcp.py +43 -0
  55. agent_blast_radius-0.1.0/src/agent_blast_radius/parsers/terraform.py +155 -0
  56. agent_blast_radius-0.1.0/src/agent_blast_radius/reach.py +67 -0
  57. agent_blast_radius-0.1.0/src/agent_blast_radius/report/__init__.py +1 -0
  58. agent_blast_radius-0.1.0/src/agent_blast_radius/report/schema.py +170 -0
  59. agent_blast_radius-0.1.0/src/agent_blast_radius/report/terminal.py +120 -0
  60. agent_blast_radius-0.1.0/src/agent_blast_radius/rules/__init__.py +1 -0
  61. agent_blast_radius-0.1.0/src/agent_blast_radius/rules/binding.py +142 -0
  62. agent_blast_radius-0.1.0/src/agent_blast_radius/rules/engine.py +116 -0
  63. agent_blast_radius-0.1.0/src/agent_blast_radius/rules/loader.py +158 -0
  64. agent_blast_radius-0.1.0/src/agent_blast_radius/validate/__init__.py +12 -0
  65. agent_blast_radius-0.1.0/src/agent_blast_radius/validate/corpus.py +103 -0
  66. agent_blast_radius-0.1.0/src/agent_blast_radius/validate/draws.py +294 -0
  67. agent_blast_radius-0.1.0/src/agent_blast_radius/validate/matrix.py +117 -0
  68. agent_blast_radius-0.1.0/src/agent_blast_radius/validate/run.py +110 -0
  69. agent_blast_radius-0.1.0/src/agent_blast_radius/validate/simulate.py +117 -0
  70. agent_blast_radius-0.1.0/tests/iam/__init__.py +0 -0
  71. agent_blast_radius-0.1.0/tests/iam/test_actions.py +50 -0
  72. agent_blast_radius-0.1.0/tests/iam/test_arn.py +71 -0
  73. agent_blast_radius-0.1.0/tests/iam/test_conditions.py +43 -0
  74. agent_blast_radius-0.1.0/tests/iam/test_resolver.py +190 -0
  75. agent_blast_radius-0.1.0/tests/parsers/__init__.py +0 -0
  76. agent_blast_radius-0.1.0/tests/parsers/test_parsers.py +164 -0
  77. agent_blast_radius-0.1.0/tests/rules/__init__.py +0 -0
  78. agent_blast_radius-0.1.0/tests/rules/test_binding.py +181 -0
  79. agent_blast_radius-0.1.0/tests/test_cli.py +113 -0
  80. agent_blast_radius-0.1.0/tests/test_ir.py +153 -0
  81. agent_blast_radius-0.1.0/tests/validate/__init__.py +0 -0
  82. agent_blast_radius-0.1.0/tests/validate/test_harness.py +126 -0
  83. agent_blast_radius-0.1.0/validate/corpus.txt +59 -0
  84. agent_blast_radius-0.1.0/validate/results/README.md +34 -0
@@ -0,0 +1,59 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: uv pip install --system -e '.[dev]'
21
+ - run: ruff check .
22
+ - run: ruff format --check .
23
+ - run: pytest -q
24
+
25
+ # The fixture must fail its own policy with a findings failure — exit code exactly 1.
26
+ # An unsupported-only failure (2) or a mixed one (3) is a regression, not a pass.
27
+ fixture-gate:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: astral-sh/setup-uv@v5
32
+ - uses: actions/setup-python@v5
33
+ with:
34
+ python-version: "3.12"
35
+ - run: uv pip install --system -e .
36
+ - name: scan the fixture and require exit code 1
37
+ run: |
38
+ set +e
39
+ agent-blast-radius scan fixtures/overprivileged-agent --json report.json
40
+ code=$?
41
+ set -e
42
+ echo "exit code: $code"
43
+ test "$code" -eq 1
44
+ - uses: actions/upload-artifact@v4
45
+ with:
46
+ name: fixture-report
47
+ path: report.json
48
+
49
+ # The wheel must install and run cold, with no repo checkout, from uvx.
50
+ cold-install:
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+ - uses: astral-sh/setup-uv@v5
55
+ - run: uv build
56
+ - run: |
57
+ set +e
58
+ uvx --from ./dist/*.whl agent-blast-radius scan fixtures/overprivileged-agent -q
59
+ test $? -eq 1
@@ -0,0 +1,50 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v5
13
+ - run: uv build
14
+ - name: the wheel must run cold and fail the fixture with exit code 1
15
+ run: |
16
+ set +e
17
+ uvx --from ./dist/*.whl agent-blast-radius scan fixtures/overprivileged-agent -q
18
+ test $? -eq 1
19
+ - uses: actions/upload-artifact@v4
20
+ with:
21
+ name: dist
22
+ path: dist/
23
+
24
+ publish:
25
+ needs: build
26
+ runs-on: ubuntu-latest
27
+ environment: pypi
28
+ permissions:
29
+ id-token: write # PyPI trusted publishing; no API token stored anywhere
30
+ steps:
31
+ - uses: actions/download-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
36
+
37
+ github-release:
38
+ needs: build
39
+ runs-on: ubuntu-latest
40
+ permissions:
41
+ contents: write
42
+ steps:
43
+ - uses: actions/download-artifact@v4
44
+ with:
45
+ name: dist
46
+ path: dist/
47
+ - uses: softprops/action-gh-release@v2
48
+ with:
49
+ files: dist/*
50
+ generate_release_notes: true
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .mypy_cache/
13
+ .coverage
14
+ htmlcov/
15
+
16
+ # Terraform
17
+ .terraform/
18
+ # (lock file committed for the fixture so plan.json is reproducible)
19
+ *.tfstate
20
+ *.tfstate.*
21
+ *.tfplan
22
+ crash.log
23
+ override.tf
24
+ override.tf.json
25
+ *_override.tf
26
+ *_override.tf.json
27
+
28
+ # Credentials — never commit, even dead ones (plan §11)
29
+ .env
30
+ *.pem
31
+ *.tfvars
32
+ !example.tfvars
33
+ credentials
34
+ .aws/
35
+
36
+ # OS / editor
37
+ .DS_Store
38
+ .idea/
39
+ .vscode/
40
+ *.swp
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-01
4
+
5
+ First release. Everything the project plan called v0.1:
6
+
7
+ - IAM resolver over a pinned, vendored snapshot of the Service Authorization Reference
8
+ (iam-dataset, not botocore — `iam:PassRole` is not an API operation). Identity and
9
+ attached managed policies, wildcard expansion, field-wise ARN matching, four modeled
10
+ condition operators with residue for the rest, Deny subtraction with subsumption.
11
+ - `NotAction`/`NotResource` refused per statement and recorded, never approximated.
12
+ - Trust policies first-class; the PassRole chain requires the trust fact to fire.
13
+ - Taint propagation over per-tool gating and `returns_external_data`.
14
+ - Escalation rule pack: 13 cited rules as bound hyperedges; every substitution
15
+ enumerated, provenance path per firing, depth per principal.
16
+ - Parsers: Terraform plan JSON, MCP `tools/list`, Bedrock action groups; annotation
17
+ overlay with validation (every tool annotated; gating never assumed).
18
+ - Report schema 1.0.0, terminal rendering from it, no score.
19
+ - CI mode with `fail_if` gates and independent exit codes for findings vs incomplete.
20
+ - Differential validation harness against `iam:SimulateCustomPolicy`: 44-policy corpus,
21
+ stratified boundary-weighted draws, confusion matrix.
22
+ - Fixture: a Bedrock-style agent with six Lambda-backed tools, two gated, whose chain
23
+ cannot be reproduced by resolving any single role.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ali El-Asmar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ FIXTURE := fixtures/overprivileged-agent
2
+
3
+ .PHONY: test lint fixture-plan
4
+
5
+ test:
6
+ pytest -q
7
+
8
+ lint:
9
+ ruff check . && ruff format --check .
10
+
11
+ # Regenerate the checked-in Terraform plan JSON. Needs no credentials and deploys nothing.
12
+ fixture-plan:
13
+ cd $(FIXTURE)/terraform && terraform init -input=false >/dev/null \
14
+ && terraform plan -input=false -out=tfplan >/dev/null \
15
+ && terraform show -json tfplan > ../plan.json && rm -f tfplan
16
+ @echo "wrote $(FIXTURE)/plan.json"
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-blast-radius
3
+ Version: 0.1.0
4
+ Summary: Static, pre-deployment analysis of what an agentic system can reach in an AWS account from untrusted model input.
5
+ Project-URL: Homepage, https://github.com/aelasmar01/agent-blast-radius
6
+ Project-URL: Source, https://github.com/aelasmar01/agent-blast-radius
7
+ Author: Ali El-Asmar
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 Ali El-Asmar
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: agents,aws,bedrock,iam,mcp,privilege-escalation,security
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Programming Language :: Python :: 3.13
37
+ Classifier: Topic :: Security
38
+ Requires-Python: >=3.11
39
+ Requires-Dist: botocore>=1.34
40
+ Requires-Dist: pyyaml>=6.0
41
+ Provides-Extra: dev
42
+ Requires-Dist: pytest>=8.0; extra == 'dev'
43
+ Requires-Dist: ruff>=0.6; extra == 'dev'
44
+ Provides-Extra: validate
45
+ Requires-Dist: boto3>=1.34; extra == 'validate'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # agent-blast-radius
49
+
50
+ **Static, pre-deployment analysis of what an agentic system can actually do to your AWS account.**
51
+
52
+ > This agent's four tools look scoped. Three hops of IAM later, a prompt injection is an account takeover.
53
+
54
+ That sentence is the whole project. Everything in this repo exists to make it provable against a
55
+ real fixture, in CI, before deployment — not in production telemetry after the fact.
56
+
57
+ ---
58
+
59
+ ## What it looks like
60
+
61
+ ```
62
+ $ agent-blast-radius scan ./fixtures/overprivileged-agent
63
+ agent-blast-radius deployment=overprivileged-agent account=000000000000
64
+ report schema 1.0.0 dataset 8e8e0df0ce50 rules v1
65
+
66
+ TOOLS
67
+ reachable read_support_ticket role=ticket-reader-role tainted input: ticket_id
68
+ reachable query_customer_record role=customer-lookup-role output of read_support_ticket re-enters the model context
69
+ reachable call_internal_api role=internal-api-role output of read_support_ticket re-enters the model context
70
+ reachable deploy_helper role=agent-execution-role output of read_support_ticket re-enters the model context
71
+ unreachable run_maintenance_job role=agent-execution-role gated: approval_required
72
+ unreachable rotate_credentials role=incident-response-role gated: approval_required
73
+
74
+ PRINCIPALS REACHABLE FROM ATTACKER INPUT
75
+ depth 0 agent-execution-role taint-reachable
76
+ depth 0 customer-lookup-role taint-reachable
77
+ depth 0 internal-api-role taint-reachable
78
+ depth 0 ticket-reader-role taint-reachable
79
+ depth 1 incident-response-role via 1 escalation hop
80
+
81
+ ACCOUNT TAKEOVER
82
+ Attach AdministratorAccess to a reachable role [iam-attachrolepolicy-self, rhino-2018] depth 2
83
+ iam:AttachRolePolicy on * <- incident-response-role/break-glass#BreakGlass
84
+
85
+ ESCALATION CHAINS (1)
86
+ -> incident-response-role via PassRole into a new Lambda function [passrole-lambda-createfunction, rhino-2018] depth 1
87
+ iam:PassRole on arn:aws:iam::000000000000:role/* <- agent-execution-role/helper-deploy#PassRoleToLambda
88
+ lambda:CreateFunction on * <- agent-execution-role/helper-deploy#ManageHelpers
89
+ lambda:InvokeFunction on * <- agent-execution-role/helper-deploy#ManageHelpers
90
+ fact: role_trusts_service(role=incident-response-role, service=lambda.amazonaws.com)
91
+
92
+ REACHABLE CAPABILITIES (199)
93
+ ... (see the full output in docs/post.md)
94
+
95
+ UNSUPPORTED (0)
96
+ none — the analysis is complete for the constructs this tool models
97
+
98
+ Reachability is not exploitability: this is what the tool graph permits if the model
99
+ can be induced to make the calls, not a prediction that it will.
100
+
101
+ FAIL: 9 finding(s) tripped fail_if:
102
+ - 'iam:*' matches 190 actions on * as incident-response-role (depth 1): iam:AcceptDelegationRequest, iam:AddClientIDToOpenIDConnectProvider, iam:AddRoleToInstanceProfile, ... <- incident-response-role/break-glass#BreakGlass
103
+ - 'iam:*' matches iam:PassRole on arn:aws:iam::000000000000:role/* as agent-execution-role (depth 0): iam:PassRole <- agent-execution-role/helper-deploy#PassRoleToLambda
104
+ - 'kms:Decrypt' matches kms:Decrypt on * as incident-response-role (depth 1): kms:Decrypt <- incident-response-role/break-glass#BreakGlass
105
+ - escalation chain passrole-lambda-createfunction -> incident-response-role (depth 1)
106
+ - escalation chain iam-attachrolepolicy-self -> all_actions (depth 2)
107
+ - escalation chain iam-putrolepolicy-self -> all_actions (depth 2)
108
+ - chain passrole-lambda-createfunction -> incident-response-role at depth 1 <= max_chain_depth 2
109
+ - chain iam-attachrolepolicy-self -> all_actions at depth 2 <= max_chain_depth 2
110
+ - chain iam-putrolepolicy-self -> all_actions at depth 2 <= max_chain_depth 2
111
+ $ echo $?
112
+ 1
113
+ ```
114
+
115
+ ## Threat model
116
+
117
+ An attacker controls text that reaches the model: a support ticket, a scraped page, a document in a
118
+ bucket the agent reads. They cannot call AWS directly. They can only induce the model to call the
119
+ tools it already has.
120
+
121
+ The question this tool answers: **given that starting position, what set of AWS actions becomes
122
+ reachable?**
123
+
124
+ It answers it by:
125
+
126
+ 1. Marking which tool inputs are attacker-influenced (explicit annotation — no inference).
127
+ 2. Propagating taint through the tools the model can reach, respecting per-tool gating.
128
+ 3. Resolving the IAM identity *and trust* policies behind those tools into an effective capability set.
129
+ 4. Applying an escalation rule pack to a fixpoint, so multi-action chains such as
130
+ `iam:PassRole` + `lambda:CreateFunction` surface as single findings with a provenance path.
131
+
132
+ ## Reachability is not exploitability
133
+
134
+ This tool computes what the tool graph **permits** if the model can be induced to make the calls.
135
+ It does not predict whether a given model will. It is a map of unlocked doors, not a prediction of
136
+ which door someone walks through. Every finding should be read that way.
137
+
138
+ ## Prior art
139
+
140
+ This re-implements policy resolution that Cloudsplaining and PMapper already do well. I built it to
141
+ understand IAM evaluation from the inside, and extended it with taint propagation from untrusted
142
+ model input, which those tools don't model.
143
+
144
+ | Prior art | What it covers |
145
+ |---|---|
146
+ | [Cloudsplaining](https://github.com/salesforce/cloudsplaining) (Salesforce) | Least-privilege violations, wildcard detection, roles assumable by compute services |
147
+ | [PMapper](https://github.com/nccgroup/PMapper) (NCC) | Principal-to-principal privilege escalation graphs |
148
+ | [Parliament](https://github.com/duo-labs/parliament) (Duo) | Policy linting |
149
+ | Rhino Security Labs | The canonical AWS privilege-escalation method list |
150
+ | Obsidian, Grafyn, Entrust | Commercial "agent blast radius" — runtime, SaaS-identity focused |
151
+ | AgentWard | Published cross-server MCP escalation chains against `awslabs/mcp` |
152
+ | AWS Agent Toolkit | IAM condition keys distinguishing agent actions from human ones |
153
+
154
+ The open gap is not detection quality. Everything commercial here is **runtime observability**.
155
+ Nobody is doing **static, pre-deployment, CI-gating analysis over Terraform plus tool manifests**.
156
+ That is the pitch: shift-left for agent permissions — catch the chain in the PR.
157
+
158
+ ---
159
+
160
+ ## Scope
161
+
162
+ **In scope (v1)**
163
+
164
+ - IAM identity policy parsing: wildcards, resource ARNs, basic conditions.
165
+ - Trust policy parsing. `iam:PassRole` + `lambda:CreateFunction` cannot be resolved without it —
166
+ PassRole only succeeds if the target role's trust policy admits `lambda.amazonaws.com`.
167
+ - Explicit `Deny`, evaluated after the allow set is assembled.
168
+ - 10–12 Rhino escalation methods as declarative, cited rules.
169
+ - Two input formats: MCP server manifest, Bedrock agent action group.
170
+ - Taint as explicit config annotation.
171
+ - Terminal report plus versioned JSON output.
172
+ - CI mode with assertion-based failure.
173
+
174
+ **Out of scope, deliberately — ordered by impact on *this* threat model**
175
+
176
+ 1. **Resource-based policies** beyond trust policies (S3 bucket, KMS key, Lambda resource policies).
177
+ Highest impact, because cross-account reach lives here.
178
+ 2. Permission boundaries.
179
+ 3. SCPs.
180
+ 4. Session policies.
181
+
182
+ That ordering is not the conventional one. It follows from the threat model: this tool is about what
183
+ untrusted input reaches, and the largest unmodeled reach is cross-account via resource policies.
184
+
185
+ **Single-account assumption.** Stated in the IR and enforced in the analyzer. Cross-account reach
186
+ lives in the gap above.
187
+
188
+ **Gating is the load-bearing axis.** A survey of real deployments
189
+ ([docs/q2-shared-roles.md](docs/q2-shared-roles.md)) found that locally-run MCP servers and
190
+ framework-on-Lambda agents put every tool behind one credential, while Bedrock action groups and
191
+ Lambda-backed tools get per-tool roles — but *every* platform exposes per-tool gating (MCP client
192
+ `autoApprove`, Bedrock `requireConfirmation`, AgentCore policy engine). So reachability is computed
193
+ over gating first and the role graph second, and a single-role deployment with no gating annotations
194
+ is told plainly that taint propagation adds nothing, rather than being handed a re-skinned
195
+ Cloudsplaining report.
196
+
197
+ **`NotAction` / `NotResource` are refused, not approximated.** They invert the set logic and are a
198
+ silent under-report risk. The offending statement is skipped and recorded in the report's
199
+ `unsupported` section with its policy and statement ID; the scan continues, and CI fails closed on
200
+ a non-empty `unsupported` list by default. Failing visibly is defensible; under-reporting silently
201
+ is not. The same section carries attached managed policies the vendored snapshot doesn't know and
202
+ actions it doesn't recognise.
203
+
204
+ **Conditions.** `StringEquals`, `StringLike`, `ArnLike`, and `Bool` are modeled and carried on the
205
+ capability. Every other operator is recorded as residue and the capability is reported as
206
+ *unconstrained but flagged* — the conservative direction. A `Deny` only removes a capability when
207
+ it is unconditional and its resource pattern fully subsumes the capability's; a conditional or
208
+ partial `Deny` is flagged, never trusted.
209
+
210
+ **Action dataset.** Wildcards expand against a pinned snapshot of the AWS Service Authorization
211
+ Reference (via [iam-dataset](https://github.com/iann0036/iam-dataset)), not botocore. botocore lists
212
+ API operations, and IAM actions are not API operations: `iam:PassRole` and `s3:ListBucket` do not
213
+ exist there, and expanding `iam:*` from it would silently drop the action the headline finding
214
+ depends on. The snapshot commit is in `src/agent_blast_radius/data/VERSION` and printed in every
215
+ report.
216
+
217
+ **No 0–100 score.** It invites "how is that computed" and there is no good answer.
218
+
219
+ ---
220
+
221
+ ## Validation
222
+
223
+ The resolver is differential-tested against `iam:SimulateCustomPolicy` over a fixed corpus of 44
224
+ managed policies chosen for construct diversity plus the fixture's roles. Draws are stratified —
225
+ half allow-expected, half deny-expected — and the deny half is weighted toward near-misses (right
226
+ action / wrong resource, failing condition, actions just outside a wildcard boundary, explicit
227
+ Deny, `NotAction` exclusions) because uniform draws are trivially denied and would inflate the
228
+ numbers. The output is a confusion matrix, not an agreement rate; the cell that matters is
229
+ *resolver says deny, AWS says allowed*. See [validate/results](validate/results/README.md) and
230
+ [docs/divergences.md](docs/divergences.md).
231
+
232
+ ## Install and run
233
+
234
+ ```bash
235
+ uvx agent-blast-radius scan ./fixtures/overprivileged-agent
236
+ ```
237
+
238
+ No manual setup, no deployed AWS resources, no credentials. The action dataset and the managed
239
+ policy documents are vendored and pinned; a scan never touches the network.
240
+
241
+ `agent.yaml` describes the deployment either as inline IR or as **sources + annotations**:
242
+
243
+ ```yaml
244
+ sources:
245
+ terraform_plan: plan.json # roles, trust policies, Lambda -> role links
246
+ mcp_tools: mcp-tools.json # tool names + argument schemas
247
+ bedrock_action_groups: [action-group.json] # functions, requireConfirmation, shared Lambda
248
+ annotations: # per tool: what no document can say
249
+ read_support_ticket:
250
+ gating: none
251
+ tainted_inputs: [ticket_id] # validated against the tool's declared arguments
252
+ returns_external_data: true # its output re-enters the model context
253
+ run_maintenance_job: {} # gating declared by Bedrock; still needs an entry
254
+ ```
255
+
256
+ Every tool needs an annotation entry. Gating is never assumed to be `none`.
257
+
258
+ ### CI mode
259
+
260
+ ```yaml
261
+ fail_if:
262
+ reachable_actions_matching: ["iam:*", "sts:AssumeRole", "kms:Decrypt"]
263
+ escalation_chains_found: true
264
+ max_chain_depth: 2 # fail on any chain reachable in <= 2 hops
265
+ unsupported_statements: true # default: fail closed when the analysis skipped something
266
+ ```
267
+
268
+ | exit | meaning |
269
+ |---|---|
270
+ | 0 | clean |
271
+ | 1 | a findings gate tripped |
272
+ | 2 | incomplete: `unsupported` is non-empty |
273
+ | 3 | both |
274
+ | 4 | input error |
275
+
276
+ Findings and incompleteness are gated independently and never collapse into one code, so a run
277
+ that skipped a `NotAction` statement cannot pass as clean, and a real finding cannot hide behind
278
+ an incompleteness failure. The repo's own CI asserts the fixture exits with **exactly 1**.
279
+
280
+ `--json report.json` writes the versioned report (schema `1.0.0`); the terminal output is rendered
281
+ from that same model. There is no score.
282
+
283
+ ## Status
284
+
285
+ v0.1 feature-complete; the first live validation run is pending. See [docs/roadmap.md](docs/roadmap.md)
286
+ for the build order and [agent-blast-radius-analyzer-plan.md](agent-blast-radius-analyzer-plan.md)
287
+ for the full project plan.
288
+
289
+ | Component | State |
290
+ |---|---|
291
+ | IR (tools, roles, policies, taint, gating) | done |
292
+ | IAM resolver (identity + managed + trust, Deny, wildcards, conditions) | done |
293
+ | Differential validation vs `iam:SimulateCustomPolicy` | harness done; first live run pending |
294
+ | MCP / Bedrock / Terraform parsers + annotation overlay | done |
295
+ | Reachability fixpoint + rule pack (13 cited rules, bound hyperedges) | done |
296
+ | Reporting (schema 1.0.0) + CI mode with independent exit codes | done |
297
+
298
+ ## License
299
+
300
+ MIT. See [LICENSE](LICENSE).