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.
- agent_blast_radius-0.1.0/.github/workflows/ci.yml +59 -0
- agent_blast_radius-0.1.0/.github/workflows/release.yml +50 -0
- agent_blast_radius-0.1.0/.gitignore +40 -0
- agent_blast_radius-0.1.0/CHANGELOG.md +23 -0
- agent_blast_radius-0.1.0/LICENSE +21 -0
- agent_blast_radius-0.1.0/Makefile +16 -0
- agent_blast_radius-0.1.0/PKG-INFO +300 -0
- agent_blast_radius-0.1.0/README.md +253 -0
- agent_blast_radius-0.1.0/SECURITY.md +23 -0
- agent_blast_radius-0.1.0/agent-blast-radius-analyzer-plan.md +298 -0
- agent_blast_radius-0.1.0/docs/divergences.md +25 -0
- agent_blast_radius-0.1.0/docs/headline.md +17 -0
- agent_blast_radius-0.1.0/docs/post.md +149 -0
- agent_blast_radius-0.1.0/docs/q2-shared-roles.md +54 -0
- agent_blast_radius-0.1.0/docs/roadmap.md +45 -0
- agent_blast_radius-0.1.0/docs/rule-semantics.md +86 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/README.md +27 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/agent.from-sources.yaml +40 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/agent.yaml +142 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/bedrock-action-group.json +33 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/mcp-tools.json +42 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/plan.json +1 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/.terraform.lock.hcl +25 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/README.md +23 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/crown_jewel.tf +5 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/outputs.tf +11 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/roles.tf +123 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/stub.zip +0 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/tools.tf +61 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/trust.tf +34 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/variables.tf +15 -0
- agent_blast_radius-0.1.0/fixtures/overprivileged-agent/terraform/versions.tf +21 -0
- agent_blast_radius-0.1.0/pyproject.toml +54 -0
- agent_blast_radius-0.1.0/scripts/build_action_dataset.py +162 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/__init__.py +10 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/analyze.py +20 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/ci.py +130 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/cli.py +146 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/data/VERSION +6 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/data/actions.json.gz +0 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/data/managed_policies.json.gz +0 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/data/rules/escalation.yaml +221 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/errors.py +34 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/iam/__init__.py +1 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/iam/actions.py +102 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/iam/arn.py +115 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/iam/conditions.py +77 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/iam/managed.py +35 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/iam/resolver.py +181 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/ir.py +345 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/loaders.py +163 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/parsers/__init__.py +34 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/parsers/bedrock.py +81 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/parsers/mcp.py +43 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/parsers/terraform.py +155 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/reach.py +67 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/report/__init__.py +1 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/report/schema.py +170 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/report/terminal.py +120 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/rules/__init__.py +1 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/rules/binding.py +142 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/rules/engine.py +116 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/rules/loader.py +158 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/validate/__init__.py +12 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/validate/corpus.py +103 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/validate/draws.py +294 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/validate/matrix.py +117 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/validate/run.py +110 -0
- agent_blast_radius-0.1.0/src/agent_blast_radius/validate/simulate.py +117 -0
- agent_blast_radius-0.1.0/tests/iam/__init__.py +0 -0
- agent_blast_radius-0.1.0/tests/iam/test_actions.py +50 -0
- agent_blast_radius-0.1.0/tests/iam/test_arn.py +71 -0
- agent_blast_radius-0.1.0/tests/iam/test_conditions.py +43 -0
- agent_blast_radius-0.1.0/tests/iam/test_resolver.py +190 -0
- agent_blast_radius-0.1.0/tests/parsers/__init__.py +0 -0
- agent_blast_radius-0.1.0/tests/parsers/test_parsers.py +164 -0
- agent_blast_radius-0.1.0/tests/rules/__init__.py +0 -0
- agent_blast_radius-0.1.0/tests/rules/test_binding.py +181 -0
- agent_blast_radius-0.1.0/tests/test_cli.py +113 -0
- agent_blast_radius-0.1.0/tests/test_ir.py +153 -0
- agent_blast_radius-0.1.0/tests/validate/__init__.py +0 -0
- agent_blast_radius-0.1.0/tests/validate/test_harness.py +126 -0
- agent_blast_radius-0.1.0/validate/corpus.txt +59 -0
- 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).
|