asp-iam 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.
- asp_iam-0.1.0/.github/workflows/ci.yml +34 -0
- asp_iam-0.1.0/.github/workflows/release.yml +31 -0
- asp_iam-0.1.0/.gitignore +8 -0
- asp_iam-0.1.0/.python-version +1 -0
- asp_iam-0.1.0/CHANGELOG.md +12 -0
- asp_iam-0.1.0/LICENSE +21 -0
- asp_iam-0.1.0/PKG-INFO +154 -0
- asp_iam-0.1.0/README.md +128 -0
- asp_iam-0.1.0/examples/ai_agent.py +146 -0
- asp_iam-0.1.0/examples/quickstart.py +29 -0
- asp_iam-0.1.0/examples/terraform_output.py +25 -0
- asp_iam-0.1.0/pyproject.toml +51 -0
- asp_iam-0.1.0/src/asp/__init__.py +16 -0
- asp_iam-0.1.0/src/asp/check.py +69 -0
- asp_iam-0.1.0/src/asp/explain.py +57 -0
- asp_iam-0.1.0/src/asp/mappings.py +65 -0
- asp_iam-0.1.0/src/asp/pytest_plugin.py +75 -0
- asp_iam-0.1.0/src/asp/recorder.py +145 -0
- asp_iam-0.1.0/src/asp/resources.py +103 -0
- asp_iam-0.1.0/tests/conftest.py +1 -0
- asp_iam-0.1.0/tests/test_check.py +78 -0
- asp_iam-0.1.0/tests/test_explain.py +42 -0
- asp_iam-0.1.0/tests/test_mappings.py +70 -0
- asp_iam-0.1.0/tests/test_pytest_plugin.py +69 -0
- asp_iam-0.1.0/tests/test_recorder.py +101 -0
- asp_iam-0.1.0/tests/test_resources.py +113 -0
- asp_iam-0.1.0/uv.lock +1415 -0
|
@@ -0,0 +1,34 @@
|
|
|
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: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run pytest -q
|
|
22
|
+
|
|
23
|
+
terraform:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: astral-sh/setup-uv@v6
|
|
28
|
+
- uses: hashicorp/setup-terraform@v3
|
|
29
|
+
- run: uv sync
|
|
30
|
+
- name: Generate a policy as Terraform
|
|
31
|
+
run: uv run python examples/terraform_output.py
|
|
32
|
+
- name: Terraform says the file is valid
|
|
33
|
+
working-directory: examples/out
|
|
34
|
+
run: terraform init -backend=false && terraform validate
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Push a version tag to publish: git tag v0.1.0 && git push origin v0.1.0
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi # must match the "trusted publisher" set up on pypi.org
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # lets PyPI verify this workflow; no password or token stored
|
|
14
|
+
contents: write # to create the GitHub Release
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v6
|
|
18
|
+
- name: Tag must match the version in pyproject.toml
|
|
19
|
+
run: |
|
|
20
|
+
VERSION=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)
|
|
21
|
+
test "v$VERSION" = "${{ github.ref_name }}" || { echo "tag ${{ github.ref_name }} != v$VERSION"; exit 1; }
|
|
22
|
+
- run: uv sync
|
|
23
|
+
- name: Tests must pass before anything is published
|
|
24
|
+
run: uv run pytest -q
|
|
25
|
+
- run: uv build
|
|
26
|
+
- name: Publish to PyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
28
|
+
- name: Create GitHub Release
|
|
29
|
+
env:
|
|
30
|
+
GH_TOKEN: ${{ github.token }}
|
|
31
|
+
run: gh release create "${{ github.ref_name }}" dist/* --notes-file CHANGELOG.md
|
asp_iam-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (first release)
|
|
4
|
+
|
|
5
|
+
- `record()`: record every AWS call made through boto3 (`with` block or decorator)
|
|
6
|
+
- correct IAM action names where API and IAM names differ (`mappings.py`)
|
|
7
|
+
- policies scoped to the exact S3 buckets, DynamoDB tables/indexes, SQS queues, SNS topics and Lambda functions used
|
|
8
|
+
- `rec.terraform()`: the policy as a Terraform `aws_iam_policy`
|
|
9
|
+
- `rec.check(policy)`: missing / unused / too-broad permissions in an existing policy
|
|
10
|
+
- `rec.explain()`: the policy in plain English
|
|
11
|
+
- pytest plugin: `pytest --asp`, `pytest --asp-check role.json`
|
|
12
|
+
- example: an AI agent with AWS tools gets a least-privilege policy
|
asp_iam-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kxvya18
|
|
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.
|
asp_iam-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: asp-iam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent Scoped Permissions: record the AWS calls your code (or AI agent) makes and get the least-privilege IAM policy
|
|
5
|
+
Project-URL: Homepage, https://github.com/Kxvya18/asp
|
|
6
|
+
Project-URL: Issues, https://github.com/Kxvya18/asp/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/Kxvya18/asp/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Kxvya18
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,aws,boto3,devsecops,iam,least-privilege,security
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: boto3>=1.26
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# asp: Agent Scoped Permissions
|
|
28
|
+
|
|
29
|
+
[](https://github.com/Kxvya18/asp/actions/workflows/ci.yml)
|
|
30
|
+
[](https://pypi.org/project/asp-iam/)
|
|
31
|
+
[](https://pypi.org/project/asp-iam/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
Record the AWS calls your Python code, or your **AI agent**, actually makes, and get the
|
|
35
|
+
**least-privilege IAM policy** for it.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from asp import record
|
|
39
|
+
|
|
40
|
+
with record() as rec:
|
|
41
|
+
run_my_script() # or: agent.run("archive last month's reports")
|
|
42
|
+
|
|
43
|
+
print(rec.policy_json())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"Version": "2012-10-17",
|
|
49
|
+
"Statement": [
|
|
50
|
+
{"Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::reports/*"},
|
|
51
|
+
{"Effect": "Allow", "Action": ["s3:PutObject"], "Resource": "arn:aws:s3:::archive/*"}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
It only reads from `reports` and only writes to `archive`, because that's all the code did.
|
|
56
|
+
|
|
57
|
+
## Why
|
|
58
|
+
AI agents and scripts are often given broad AWS permissions because nobody knows what they
|
|
59
|
+
really need. `asp` watches what they actually do and writes the smallest policy that allows it.
|
|
60
|
+
It works offline with [moto](https://github.com/getmoto/moto), so it fits in your tests.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
```bash
|
|
64
|
+
pip install asp-iam
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Use
|
|
68
|
+
```python
|
|
69
|
+
from asp import record
|
|
70
|
+
|
|
71
|
+
# as a block
|
|
72
|
+
with record() as rec:
|
|
73
|
+
...
|
|
74
|
+
rec.actions() # ['dynamodb:PutItem', 's3:GetObject']
|
|
75
|
+
rec.policy() # dict
|
|
76
|
+
rec.save("policy.json")
|
|
77
|
+
rec.terraform() # the same policy as a Terraform resource
|
|
78
|
+
|
|
79
|
+
# as a decorator
|
|
80
|
+
@record(save_to="policy.json")
|
|
81
|
+
def nightly_job():
|
|
82
|
+
...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Correct permission names
|
|
86
|
+
Python call names aren't always permission names: `list_objects_v2` needs `s3:ListBucket`,
|
|
87
|
+
`copy_object` needs `s3:GetObject` + `s3:PutObject`, CloudWatch calls need `cloudwatch:*`,
|
|
88
|
+
Bedrock's `Converse` needs `bedrock:InvokeModel`. These exceptions live in one readable table,
|
|
89
|
+
[`mappings.py`](src/asp/mappings.py). Found a missing one? Add a row and a test.
|
|
90
|
+
|
|
91
|
+
## AI agents: give them only what they use
|
|
92
|
+
[`examples/ai_agent.py`](examples/ai_agent.py) gives an agent three AWS tools (list, read, write
|
|
93
|
+
files) and the task *"summarise the October report and save it to the archive"*. Recorded with asp:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
The agent needed only this:
|
|
97
|
+
- read files in bucket 'reports'
|
|
98
|
+
- list files in bucket 'reports'
|
|
99
|
+
- write files in bucket 'archive'
|
|
100
|
+
```
|
|
101
|
+
Instead of broad S3 access "to be safe", the agent gets exactly this. If it is ever misled (for
|
|
102
|
+
example by instructions hidden in a file it reads), it cannot delete data or touch other buckets.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python examples/ai_agent.py # scripted stand-in, no API key, fake AWS
|
|
106
|
+
python examples/ai_agent.py --claude # real Claude drives the agent (pip install anthropic)
|
|
107
|
+
```
|
|
108
|
+
`rec.explain()` produces the plain-English summary with ordinary code, not an LLM, so it is
|
|
109
|
+
always exactly what the policy says.
|
|
110
|
+
|
|
111
|
+
## From your test suite (pytest plugin)
|
|
112
|
+
```bash
|
|
113
|
+
pytest --asp # writes asp-policy.json: the permissions your tests needed
|
|
114
|
+
pytest --asp-check role.json # fails the run if the code needs actions role.json doesn't allow
|
|
115
|
+
```
|
|
116
|
+
Put `--asp-check` in CI and a change that suddenly needs a new AWS permission is caught before
|
|
117
|
+
it fails in production. Only calls made inside test bodies are recorded: put setup (like creating
|
|
118
|
+
fake buckets with moto) in fixtures so it stays out of the policy.
|
|
119
|
+
|
|
120
|
+
## Check an existing policy
|
|
121
|
+
```python
|
|
122
|
+
result = rec.check("role_policy.json") # dict, JSON text or a file path
|
|
123
|
+
print(result.summary())
|
|
124
|
+
```
|
|
125
|
+
```
|
|
126
|
+
missing (code would fail): 0
|
|
127
|
+
unused (can be removed): 2
|
|
128
|
+
- dynamodb:Scan
|
|
129
|
+
- iam:CreateUser
|
|
130
|
+
too broad (wildcards): 1
|
|
131
|
+
- s3:*
|
|
132
|
+
```
|
|
133
|
+
`result.ok` is False when the code needs something the policy doesn't allow, which is handy in CI.
|
|
134
|
+
This first version compares actions, not resources.
|
|
135
|
+
|
|
136
|
+
## Status
|
|
137
|
+
Early (v0.x). Policies name the exact actions and, for the services above, the exact resources.
|
|
138
|
+
Other services keep `"Resource": "*"`. `rec.policy(scope_resources=False)` gives the old all-`*` form.
|
|
139
|
+
|
|
140
|
+
Roadmap:
|
|
141
|
+
- [x] Terraform output (`rec.terraform()`)
|
|
142
|
+
- [x] exact IAM names where API and IAM names differ (`src/asp/mappings.py`, contributions welcome)
|
|
143
|
+
- [x] restrict `Resource` to the exact S3 buckets, DynamoDB tables (and indexes), SQS queues, SNS topics, Lambda functions used
|
|
144
|
+
- [x] `check` mode: compare an existing policy with what the code really needs (actions)
|
|
145
|
+
- [x] AI-agent example and plain-English `explain()`
|
|
146
|
+
- [x] pytest plugin: `pytest --asp` / `pytest --asp-check role.json`
|
|
147
|
+
|
|
148
|
+
## Contributing
|
|
149
|
+
Found an AWS call that gets the wrong permission name? Add one row to
|
|
150
|
+
[`mappings.py`](src/asp/mappings.py) plus a test, and open a PR. Issues labelled
|
|
151
|
+
**good first issue** are a good place to start.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
MIT
|
asp_iam-0.1.0/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# asp: Agent Scoped Permissions
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Kxvya18/asp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/asp-iam/)
|
|
5
|
+
[](https://pypi.org/project/asp-iam/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Record the AWS calls your Python code, or your **AI agent**, actually makes, and get the
|
|
9
|
+
**least-privilege IAM policy** for it.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from asp import record
|
|
13
|
+
|
|
14
|
+
with record() as rec:
|
|
15
|
+
run_my_script() # or: agent.run("archive last month's reports")
|
|
16
|
+
|
|
17
|
+
print(rec.policy_json())
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"Version": "2012-10-17",
|
|
23
|
+
"Statement": [
|
|
24
|
+
{"Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::reports/*"},
|
|
25
|
+
{"Effect": "Allow", "Action": ["s3:PutObject"], "Resource": "arn:aws:s3:::archive/*"}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
It only reads from `reports` and only writes to `archive`, because that's all the code did.
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
AI agents and scripts are often given broad AWS permissions because nobody knows what they
|
|
33
|
+
really need. `asp` watches what they actually do and writes the smallest policy that allows it.
|
|
34
|
+
It works offline with [moto](https://github.com/getmoto/moto), so it fits in your tests.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
```bash
|
|
38
|
+
pip install asp-iam
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use
|
|
42
|
+
```python
|
|
43
|
+
from asp import record
|
|
44
|
+
|
|
45
|
+
# as a block
|
|
46
|
+
with record() as rec:
|
|
47
|
+
...
|
|
48
|
+
rec.actions() # ['dynamodb:PutItem', 's3:GetObject']
|
|
49
|
+
rec.policy() # dict
|
|
50
|
+
rec.save("policy.json")
|
|
51
|
+
rec.terraform() # the same policy as a Terraform resource
|
|
52
|
+
|
|
53
|
+
# as a decorator
|
|
54
|
+
@record(save_to="policy.json")
|
|
55
|
+
def nightly_job():
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Correct permission names
|
|
60
|
+
Python call names aren't always permission names: `list_objects_v2` needs `s3:ListBucket`,
|
|
61
|
+
`copy_object` needs `s3:GetObject` + `s3:PutObject`, CloudWatch calls need `cloudwatch:*`,
|
|
62
|
+
Bedrock's `Converse` needs `bedrock:InvokeModel`. These exceptions live in one readable table,
|
|
63
|
+
[`mappings.py`](src/asp/mappings.py). Found a missing one? Add a row and a test.
|
|
64
|
+
|
|
65
|
+
## AI agents: give them only what they use
|
|
66
|
+
[`examples/ai_agent.py`](examples/ai_agent.py) gives an agent three AWS tools (list, read, write
|
|
67
|
+
files) and the task *"summarise the October report and save it to the archive"*. Recorded with asp:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
The agent needed only this:
|
|
71
|
+
- read files in bucket 'reports'
|
|
72
|
+
- list files in bucket 'reports'
|
|
73
|
+
- write files in bucket 'archive'
|
|
74
|
+
```
|
|
75
|
+
Instead of broad S3 access "to be safe", the agent gets exactly this. If it is ever misled (for
|
|
76
|
+
example by instructions hidden in a file it reads), it cannot delete data or touch other buckets.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python examples/ai_agent.py # scripted stand-in, no API key, fake AWS
|
|
80
|
+
python examples/ai_agent.py --claude # real Claude drives the agent (pip install anthropic)
|
|
81
|
+
```
|
|
82
|
+
`rec.explain()` produces the plain-English summary with ordinary code, not an LLM, so it is
|
|
83
|
+
always exactly what the policy says.
|
|
84
|
+
|
|
85
|
+
## From your test suite (pytest plugin)
|
|
86
|
+
```bash
|
|
87
|
+
pytest --asp # writes asp-policy.json: the permissions your tests needed
|
|
88
|
+
pytest --asp-check role.json # fails the run if the code needs actions role.json doesn't allow
|
|
89
|
+
```
|
|
90
|
+
Put `--asp-check` in CI and a change that suddenly needs a new AWS permission is caught before
|
|
91
|
+
it fails in production. Only calls made inside test bodies are recorded: put setup (like creating
|
|
92
|
+
fake buckets with moto) in fixtures so it stays out of the policy.
|
|
93
|
+
|
|
94
|
+
## Check an existing policy
|
|
95
|
+
```python
|
|
96
|
+
result = rec.check("role_policy.json") # dict, JSON text or a file path
|
|
97
|
+
print(result.summary())
|
|
98
|
+
```
|
|
99
|
+
```
|
|
100
|
+
missing (code would fail): 0
|
|
101
|
+
unused (can be removed): 2
|
|
102
|
+
- dynamodb:Scan
|
|
103
|
+
- iam:CreateUser
|
|
104
|
+
too broad (wildcards): 1
|
|
105
|
+
- s3:*
|
|
106
|
+
```
|
|
107
|
+
`result.ok` is False when the code needs something the policy doesn't allow, which is handy in CI.
|
|
108
|
+
This first version compares actions, not resources.
|
|
109
|
+
|
|
110
|
+
## Status
|
|
111
|
+
Early (v0.x). Policies name the exact actions and, for the services above, the exact resources.
|
|
112
|
+
Other services keep `"Resource": "*"`. `rec.policy(scope_resources=False)` gives the old all-`*` form.
|
|
113
|
+
|
|
114
|
+
Roadmap:
|
|
115
|
+
- [x] Terraform output (`rec.terraform()`)
|
|
116
|
+
- [x] exact IAM names where API and IAM names differ (`src/asp/mappings.py`, contributions welcome)
|
|
117
|
+
- [x] restrict `Resource` to the exact S3 buckets, DynamoDB tables (and indexes), SQS queues, SNS topics, Lambda functions used
|
|
118
|
+
- [x] `check` mode: compare an existing policy with what the code really needs (actions)
|
|
119
|
+
- [x] AI-agent example and plain-English `explain()`
|
|
120
|
+
- [x] pytest plugin: `pytest --asp` / `pytest --asp-check role.json`
|
|
121
|
+
|
|
122
|
+
## Contributing
|
|
123
|
+
Found an AWS call that gets the wrong permission name? Add one row to
|
|
124
|
+
[`mappings.py`](src/asp/mappings.py) plus a test, and open a PR. Issues labelled
|
|
125
|
+
**good first issue** are a good place to start.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
MIT
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""An AI agent with AWS tools, recorded by asp. Its least-privilege policy comes out at the end.
|
|
2
|
+
|
|
3
|
+
uv run python examples/ai_agent.py # scripted stand-in, no API key needed
|
|
4
|
+
uv run python examples/ai_agent.py --claude # real Claude drives the agent
|
|
5
|
+
|
|
6
|
+
--claude needs `pip install anthropic` and Anthropic credentials.
|
|
7
|
+
|
|
8
|
+
AWS is faked with moto either way, so nothing touches a real account.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import boto3
|
|
16
|
+
from moto import mock_aws
|
|
17
|
+
|
|
18
|
+
from asp import record
|
|
19
|
+
|
|
20
|
+
os.environ.setdefault("AWS_ACCESS_KEY_ID", "testing")
|
|
21
|
+
os.environ.setdefault("AWS_SECRET_ACCESS_KEY", "testing")
|
|
22
|
+
|
|
23
|
+
TASK = (
|
|
24
|
+
"Summarise the October report in the 'reports' bucket in one sentence and "
|
|
25
|
+
"save the summary to the 'archive' bucket as october-summary.txt."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ---- the agent's tools: plain Python functions that use boto3 ---------------------------
|
|
29
|
+
|
|
30
|
+
s3 = None # created inside mock_aws below
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def list_files(bucket):
|
|
34
|
+
return [o["Key"] for o in s3.list_objects_v2(Bucket=bucket).get("Contents", [])]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_file(bucket, key):
|
|
38
|
+
return s3.get_object(Bucket=bucket, Key=key)["Body"].read().decode()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def write_file(bucket, key, text):
|
|
42
|
+
s3.put_object(Bucket=bucket, Key=key, Body=text.encode())
|
|
43
|
+
return "saved"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
TOOLS = {"list_files": list_files, "read_file": read_file, "write_file": write_file}
|
|
47
|
+
|
|
48
|
+
TOOL_SCHEMAS = [
|
|
49
|
+
{
|
|
50
|
+
"name": "list_files",
|
|
51
|
+
"description": "List the files in an S3 bucket.",
|
|
52
|
+
"input_schema": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {"bucket": {"type": "string"}},
|
|
55
|
+
"required": ["bucket"],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "read_file",
|
|
60
|
+
"description": "Read a text file from an S3 bucket.",
|
|
61
|
+
"input_schema": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": {"bucket": {"type": "string"}, "key": {"type": "string"}},
|
|
64
|
+
"required": ["bucket", "key"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "write_file",
|
|
69
|
+
"description": "Write a text file to an S3 bucket.",
|
|
70
|
+
"input_schema": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"bucket": {"type": "string"},
|
|
74
|
+
"key": {"type": "string"},
|
|
75
|
+
"text": {"type": "string"},
|
|
76
|
+
},
|
|
77
|
+
"required": ["bucket", "key", "text"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---- two ways to drive the agent ----------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run_scripted():
|
|
87
|
+
"""What a well-behaved agent does for TASK, without calling an LLM."""
|
|
88
|
+
files = list_files("reports")
|
|
89
|
+
report = read_file("reports", files[0])
|
|
90
|
+
lines = report.strip().splitlines()[1:]
|
|
91
|
+
write_file(
|
|
92
|
+
"archive",
|
|
93
|
+
"october-summary.txt",
|
|
94
|
+
f"October: {len(lines)} sites shipped {sum(int(r.split(',')[1]) for r in lines)} pallets.",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run_with_claude():
|
|
99
|
+
"""A plain agent loop: ask Claude, run the tools it asks for, send results back."""
|
|
100
|
+
import anthropic
|
|
101
|
+
|
|
102
|
+
client = anthropic.Anthropic()
|
|
103
|
+
messages = [{"role": "user", "content": TASK}]
|
|
104
|
+
while True:
|
|
105
|
+
response = client.beta.messages.create(
|
|
106
|
+
model="claude-opus-5-5",
|
|
107
|
+
max_tokens=16000,
|
|
108
|
+
tools=TOOL_SCHEMAS,
|
|
109
|
+
messages=messages,
|
|
110
|
+
betas=["server-side-fallback-2026-07-01"],
|
|
111
|
+
fallbacks="default", # if the model declines, the API retries on a fallback model
|
|
112
|
+
)
|
|
113
|
+
if response.stop_reason == "refusal":
|
|
114
|
+
sys.exit("The model declined the task.")
|
|
115
|
+
if response.stop_reason != "tool_use":
|
|
116
|
+
return next((b.text for b in response.content if b.type == "text"), "")
|
|
117
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
118
|
+
results = []
|
|
119
|
+
for block in response.content:
|
|
120
|
+
if block.type == "tool_use":
|
|
121
|
+
output = TOOLS[block.name](**block.input)
|
|
122
|
+
results.append(
|
|
123
|
+
{"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(output)}
|
|
124
|
+
)
|
|
125
|
+
messages.append({"role": "user", "content": results})
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
with mock_aws():
|
|
130
|
+
s3 = boto3.client("s3", region_name="us-east-1")
|
|
131
|
+
for name in ("reports", "archive"):
|
|
132
|
+
s3.create_bucket(Bucket=name)
|
|
133
|
+
s3.put_object(
|
|
134
|
+
Bucket="reports",
|
|
135
|
+
Key="october.csv",
|
|
136
|
+
Body=b"site,pallets\nS01,1200\nS02,2100\nS03,1800\n",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
with record() as rec: # <- everything the agent does on AWS is recorded
|
|
140
|
+
if "--claude" in sys.argv:
|
|
141
|
+
print("Claude says:", run_with_claude())
|
|
142
|
+
else:
|
|
143
|
+
run_scripted()
|
|
144
|
+
|
|
145
|
+
print("\nThe agent needed only this:\n" + rec.explain())
|
|
146
|
+
print("\nLeast-privilege policy for the agent:\n" + rec.policy_json())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Run: uv run python examples/quickstart.py (no AWS account needed: moto fakes AWS)"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
from moto import mock_aws
|
|
7
|
+
|
|
8
|
+
from asp import record
|
|
9
|
+
|
|
10
|
+
os.environ.setdefault("AWS_ACCESS_KEY_ID", "testing")
|
|
11
|
+
os.environ.setdefault("AWS_SECRET_ACCESS_KEY", "testing")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def archive_report(s3):
|
|
15
|
+
"""A tiny 'job': copy a report into an archive bucket."""
|
|
16
|
+
body = s3.get_object(Bucket="reports", Key="october.csv")["Body"].read()
|
|
17
|
+
s3.put_object(Bucket="archive", Key="2026/october.csv", Body=body)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
with mock_aws():
|
|
21
|
+
s3 = boto3.client("s3", region_name="us-east-1")
|
|
22
|
+
for name in ("reports", "archive"):
|
|
23
|
+
s3.create_bucket(Bucket=name)
|
|
24
|
+
s3.put_object(Bucket="reports", Key="october.csv", Body=b"site,pallets\nS01,1200\n")
|
|
25
|
+
|
|
26
|
+
with record() as rec:
|
|
27
|
+
archive_report(s3)
|
|
28
|
+
|
|
29
|
+
print(rec.policy_json())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Write the recorded policy as Terraform: uv run python examples/terraform_output.py
|
|
2
|
+
Creates examples/out/policy.tf (CI checks it with `terraform validate`)."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
from moto import mock_aws
|
|
9
|
+
|
|
10
|
+
from asp import record
|
|
11
|
+
|
|
12
|
+
os.environ.setdefault("AWS_ACCESS_KEY_ID", "testing")
|
|
13
|
+
os.environ.setdefault("AWS_SECRET_ACCESS_KEY", "testing")
|
|
14
|
+
|
|
15
|
+
with mock_aws():
|
|
16
|
+
s3 = boto3.client("s3", region_name="us-east-1")
|
|
17
|
+
s3.create_bucket(Bucket="reports")
|
|
18
|
+
with record() as rec:
|
|
19
|
+
s3.put_object(Bucket="reports", Key="a.csv", Body=b"x")
|
|
20
|
+
s3.get_object(Bucket="reports", Key="a.csv")
|
|
21
|
+
|
|
22
|
+
out = Path(__file__).parent / "out"
|
|
23
|
+
out.mkdir(exist_ok=True)
|
|
24
|
+
(out / "policy.tf").write_text(rec.terraform(policy_name="reports-writer"))
|
|
25
|
+
print((out / "policy.tf").read_text())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "asp-iam"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Agent Scoped Permissions: record the AWS calls your code (or AI agent) makes and get the least-privilege IAM policy"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [{ name = "Kxvya18" }]
|
|
9
|
+
keywords = ["aws", "iam", "least-privilege", "security", "boto3", "ai-agents", "devsecops"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Framework :: Pytest",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
"Topic :: System :: Systems Administration",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["boto3>=1.26"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/Kxvya18/asp"
|
|
27
|
+
Issues = "https://github.com/Kxvya18/asp/issues"
|
|
28
|
+
Changelog = "https://github.com/Kxvya18/asp/blob/main/CHANGELOG.md"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = ["pytest>=8", "moto[s3,dynamodb,sqs]>=5", "ruff>=0.5"]
|
|
32
|
+
examples = [
|
|
33
|
+
"anthropic>=1.13.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/asp"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 100
|
|
45
|
+
target-version = "py310"
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
49
|
+
|
|
50
|
+
[project.entry-points.pytest11]
|
|
51
|
+
asp = "asp.pytest_plugin"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""asp - Agent Scoped Permissions.
|
|
2
|
+
|
|
3
|
+
Record the AWS calls your code (or AI agent) makes and get the least-privilege IAM policy.
|
|
4
|
+
|
|
5
|
+
from asp import record
|
|
6
|
+
|
|
7
|
+
with record() as rec:
|
|
8
|
+
run_my_script()
|
|
9
|
+
print(rec.policy_json())
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from asp.check import CheckResult, check
|
|
13
|
+
from asp.recorder import Call, Recorder, record
|
|
14
|
+
|
|
15
|
+
__all__ = ["Call", "CheckResult", "Recorder", "check", "record"]
|
|
16
|
+
__version__ = "0.1.0"
|