cfn2iam 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.
@@ -0,0 +1,20 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v4
17
+ - name: Build and publish
18
+ run: |
19
+ uv build
20
+ uv publish
@@ -0,0 +1,209 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
208
+
209
+ policy-*
cfn2iam-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Murali Krishnan
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.
cfn2iam-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: cfn2iam
3
+ Version: 0.1.0
4
+ Summary: A tool to automatically generate minimal IAM policy to deploy a CloudFormation stack from its template.
5
+ Project-URL: Homepage, https://github.com/mrlikl/cfn2iam.git
6
+ Author-email: S Murali Krishnan <mrlikrsh@gmail.com>
7
+ License-File: LICENSE
8
+ Keywords: aws, cloudformation, iam, permissions
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Utilities
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: boto3
14
+ Requires-Dist: pyyaml
15
+ Requires-Dist: typer
16
+ Description-Content-Type: text/markdown
17
+
18
+ # CloudFormation to IAM (cfn2iam)
19
+
20
+ A tool to automatically generate minimal IAM policy to deploy a CloudFormation stack from its template.
21
+
22
+ ## Overview
23
+
24
+ This tool analyzes CloudFormation templates to identify all resource types used, then queries the CloudFormation registry to determine the required IAM permissions for each resource type. It can generate IAM policy documents or create IAM roles with the appropriate permissions.
25
+
26
+ ## Features
27
+
28
+ - Parse CloudFormation templates in JSON or YAML format
29
+ - Extract resource types and determine required permissions
30
+ - Generate IAM policy documents with appropriate permissions
31
+ - Create IAM roles with the generated permissions
32
+ - Option to allow or deny delete permissions
33
+ - Support for permissions boundaries
34
+
35
+ ## Prerequisites
36
+
37
+ - Python 3.9+
38
+ - AWS CLI configured with [CloudFormation DescribeType](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_DescribeType.html) permission
39
+ - [uv package manager](https://docs.astral.sh/uv/getting-started/installation/)
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ git clone https://github.com/mrlikl/cfn2iam.git
45
+ cd cfn2iam
46
+ uv sync
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ python source/app.py <template_file> [options]
53
+ # or using shorthand options
54
+ python source/app.py -t <template_file> [options]
55
+ ```
56
+
57
+ ### Arguments
58
+
59
+ - `template_file` or `-t, --template-file`: Path to the CloudFormation template file (JSON or YAML)
60
+
61
+ ### Options
62
+
63
+ - `-d, --allow-delete`: Allow delete permissions instead of denying them
64
+ - `-c, --create-role`: Create an IAM role with the generated permissions (default: True)
65
+ - `-r, --role-name`: Name for the IAM role (if not specified, uses 'cfn2iam-<random_hash>')
66
+ - `-p, --permissions-boundary`: ARN of the permissions boundary to attach to the role
67
+
68
+ ### Examples
69
+
70
+ Generate a policy document from a template:
71
+ ```bash
72
+ python source/app.py path/to/template.yaml
73
+ # or
74
+ python source/app.py -t path/to/template.yaml
75
+ ```
76
+
77
+ Create an IAM role with delete permissions denied (default behavior):
78
+ ```bash
79
+ python source/app.py path/to/template.yaml
80
+ ```
81
+
82
+ Create an IAM role with delete permissions allowed:
83
+ ```bash
84
+ python source/app.py path/to/template.yaml -d
85
+ ```
86
+
87
+ Create an IAM role with a custom name:
88
+ ```bash
89
+ python source/app.py path/to/template.yaml -r MyCustomRole
90
+ ```
91
+
92
+ Create an IAM role with a permissions boundary:
93
+ ```bash
94
+ python source/app.py path/to/template.yaml -p arn:aws:iam::123456789012:policy/boundary
95
+ ```
96
+
97
+ ## How It Works
98
+
99
+ 1. The tool parses the CloudFormation template to extract all resource types
100
+ 2. For each resource type, it queries the CloudFormation registry to get the required permissions
101
+ 3. It categorizes permissions into "update" (create/update/read) and "delete-specific" permissions
102
+ 4. It generates a policy document with appropriate Allow and Deny statements
103
+ 5. It saves the policy document to a file with a unique name
104
+ 6. If requested (default), it creates an IAM role with the generated policy
105
+
106
+ ## License
107
+
108
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,91 @@
1
+ # CloudFormation to IAM (cfn2iam)
2
+
3
+ A tool to automatically generate minimal IAM policy to deploy a CloudFormation stack from its template.
4
+
5
+ ## Overview
6
+
7
+ This tool analyzes CloudFormation templates to identify all resource types used, then queries the CloudFormation registry to determine the required IAM permissions for each resource type. It can generate IAM policy documents or create IAM roles with the appropriate permissions.
8
+
9
+ ## Features
10
+
11
+ - Parse CloudFormation templates in JSON or YAML format
12
+ - Extract resource types and determine required permissions
13
+ - Generate IAM policy documents with appropriate permissions
14
+ - Create IAM roles with the generated permissions
15
+ - Option to allow or deny delete permissions
16
+ - Support for permissions boundaries
17
+
18
+ ## Prerequisites
19
+
20
+ - Python 3.9+
21
+ - AWS CLI configured with [CloudFormation DescribeType](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_DescribeType.html) permission
22
+ - [uv package manager](https://docs.astral.sh/uv/getting-started/installation/)
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ git clone https://github.com/mrlikl/cfn2iam.git
28
+ cd cfn2iam
29
+ uv sync
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ python source/app.py <template_file> [options]
36
+ # or using shorthand options
37
+ python source/app.py -t <template_file> [options]
38
+ ```
39
+
40
+ ### Arguments
41
+
42
+ - `template_file` or `-t, --template-file`: Path to the CloudFormation template file (JSON or YAML)
43
+
44
+ ### Options
45
+
46
+ - `-d, --allow-delete`: Allow delete permissions instead of denying them
47
+ - `-c, --create-role`: Create an IAM role with the generated permissions (default: True)
48
+ - `-r, --role-name`: Name for the IAM role (if not specified, uses 'cfn2iam-<random_hash>')
49
+ - `-p, --permissions-boundary`: ARN of the permissions boundary to attach to the role
50
+
51
+ ### Examples
52
+
53
+ Generate a policy document from a template:
54
+ ```bash
55
+ python source/app.py path/to/template.yaml
56
+ # or
57
+ python source/app.py -t path/to/template.yaml
58
+ ```
59
+
60
+ Create an IAM role with delete permissions denied (default behavior):
61
+ ```bash
62
+ python source/app.py path/to/template.yaml
63
+ ```
64
+
65
+ Create an IAM role with delete permissions allowed:
66
+ ```bash
67
+ python source/app.py path/to/template.yaml -d
68
+ ```
69
+
70
+ Create an IAM role with a custom name:
71
+ ```bash
72
+ python source/app.py path/to/template.yaml -r MyCustomRole
73
+ ```
74
+
75
+ Create an IAM role with a permissions boundary:
76
+ ```bash
77
+ python source/app.py path/to/template.yaml -p arn:aws:iam::123456789012:policy/boundary
78
+ ```
79
+
80
+ ## How It Works
81
+
82
+ 1. The tool parses the CloudFormation template to extract all resource types
83
+ 2. For each resource type, it queries the CloudFormation registry to get the required permissions
84
+ 3. It categorizes permissions into "update" (create/update/read) and "delete-specific" permissions
85
+ 4. It generates a policy document with appropriate Allow and Deny statements
86
+ 5. It saves the policy document to a file with a unique name
87
+ 6. If requested (default), it creates an IAM role with the generated policy
88
+
89
+ ## License
90
+
91
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cfn2iam"
7
+ dynamic = ["version"]
8
+ description = "A tool to automatically generate minimal IAM policy to deploy a CloudFormation stack from its template."
9
+ readme = "README.md"
10
+ authors = [
11
+ {name = "S Murali Krishnan", email = "mrlikrsh@gmail.com"}
12
+ ]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Operating System :: OS Independent",
16
+ "Topic :: Utilities",
17
+ ]
18
+ license-files = [
19
+ "LICENSE",
20
+ ]
21
+ keywords = ["aws, cloudformation, iam, permissions"]
22
+ dependencies = [
23
+ "boto3",
24
+ "pyyaml",
25
+ "typer"
26
+ ]
27
+ requires-python = ">=3.9"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/mrlikl/cfn2iam.git"
31
+
32
+ [project.scripts]
33
+ cfn2iam = "source.app:app"
34
+
35
+ [tool.hatch.version]
36
+ source = "vcs"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["source"]
@@ -0,0 +1 @@
1
+ # This file is required to make the directory a Python package
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env python3
2
+ import boto3
3
+ import typer
4
+ import json
5
+ import yaml
6
+ import sys
7
+ import re
8
+ import uuid
9
+ from typing import Optional
10
+ from importlib.metadata import version
11
+
12
+
13
+ def version_callback(value: bool):
14
+ if value:
15
+ try:
16
+ pkg_version = version("cfn2iam")
17
+ except Exception:
18
+ pkg_version = "unknown"
19
+ print(f"cfn2iam {pkg_version}")
20
+ raise typer.Exit()
21
+
22
+ # Simple constructor that ignores CloudFormation intrinsic functions
23
+
24
+
25
+ def ignore_unknown_tags(loader, tag_suffix, node):
26
+ if isinstance(node, yaml.ScalarNode):
27
+ return loader.construct_scalar(node)
28
+ elif isinstance(node, yaml.SequenceNode):
29
+ return loader.construct_sequence(node)
30
+ elif isinstance(node, yaml.MappingNode):
31
+ return loader.construct_mapping(node)
32
+ return None
33
+
34
+
35
+ # Add multi constructor to handle all CloudFormation tags
36
+ yaml.SafeLoader.add_multi_constructor('!', ignore_unknown_tags)
37
+
38
+
39
+ def parse_cloudformation_template(file_path):
40
+ with open(file_path, 'r') as file:
41
+ if file_path.endswith('.json'):
42
+ template = json.load(file)
43
+ elif file_path.endswith(('.yaml', '.yml')):
44
+ template = yaml.safe_load(file)
45
+ else:
46
+ raise ValueError(
47
+ "Unsupported file format. Please provide a JSON or YAML file.")
48
+
49
+ if 'Resources' not in template:
50
+ print("No Resources section found in the template.")
51
+ return set()
52
+
53
+ ignore_patterns = [
54
+ r"^Custom::.*",
55
+ r"^AWS::CDK::Metadata",
56
+ r"^AWS::CloudFormation::CustomResource"
57
+ ]
58
+
59
+ resource_types = set()
60
+ for resource in template['Resources'].values():
61
+ if 'Type' in resource:
62
+ resource_type = resource['Type']
63
+ if not any(re.match(pattern, resource_type) for pattern in ignore_patterns):
64
+ resource_types.add(resource_type)
65
+ return resource_types
66
+
67
+
68
+ def get_permissions(resourcetype):
69
+ cfn_client = boto3.client('cloudformation')
70
+ response = cfn_client.describe_type(Type='RESOURCE', TypeName=resourcetype)
71
+ data = json.loads(response['Schema'])
72
+
73
+ iam_update = set()
74
+ iam_delete = set()
75
+
76
+ if 'handlers' in data:
77
+ handlers = data['handlers']
78
+ # Collect all non-delete permissions
79
+ for action in ['create', 'update', 'read', 'list']:
80
+ if action in handlers:
81
+ iam_update.update(handlers[action]['permissions'])
82
+
83
+ # Collect delete-only permissions
84
+ if 'delete' in handlers:
85
+ delete_perms = set(handlers['delete']['permissions'])
86
+ iam_delete = delete_perms - iam_update
87
+
88
+ return iam_update, iam_delete
89
+
90
+
91
+ def generate_random_hash():
92
+ """Generate a short random hash for role name uniqueness"""
93
+ return uuid.uuid4().hex[:8]
94
+
95
+
96
+ def generate_policy_document(all_update_permissions, all_delete_permissions, allow_delete=False):
97
+ """
98
+ Generate an IAM policy document with the specified permissions.
99
+
100
+ Args:
101
+ all_update_permissions (set): Set of permissions to allow
102
+ all_delete_permissions (set): Set of permissions to deny or allow based on allow_delete flag
103
+ allow_delete (bool): If True, include delete permissions as Allow, otherwise as Deny
104
+
105
+ Returns:
106
+ dict: Policy document
107
+ """
108
+ statements = []
109
+ if all_update_permissions:
110
+ statements.append({
111
+ "Effect": "Allow",
112
+ "Action": list(sorted(all_update_permissions)),
113
+ "Resource": "*"
114
+ })
115
+ if all_delete_permissions:
116
+ statements.append({
117
+ "Effect": "Allow" if allow_delete else "Deny",
118
+ "Action": list(sorted(all_delete_permissions)),
119
+ "Resource": "*"
120
+ })
121
+
122
+ policy_document = {
123
+ "Version": "2012-10-17",
124
+ "Statement": statements
125
+ }
126
+
127
+ return policy_document
128
+
129
+
130
+ def create_iam_role(policy_document, role_name, permissions_boundary=None):
131
+ iam_client = boto3.client('iam')
132
+ trust_policy = {
133
+ "Version": "2012-10-17",
134
+ "Statement": [
135
+ {
136
+ "Effect": "Allow",
137
+ "Principal": {
138
+ "Service": "cloudformation.amazonaws.com"
139
+ },
140
+ "Action": "sts:AssumeRole"
141
+ }
142
+ ]
143
+ }
144
+
145
+ try:
146
+ create_role_params = {
147
+ "RoleName": role_name,
148
+ "AssumeRolePolicyDocument": json.dumps(trust_policy),
149
+ "Description": "Role generated using cfn2iam"
150
+ }
151
+ if permissions_boundary:
152
+ create_role_params["PermissionsBoundary"] = permissions_boundary
153
+
154
+ response = iam_client.create_role(**create_role_params)
155
+ role_arn = response['Role']['Arn']
156
+ policy_name = f"{role_name}-Policy"
157
+ iam_client.put_role_policy(
158
+ RoleName=role_name,
159
+ PolicyName=policy_name,
160
+ PolicyDocument=json.dumps(policy_document)
161
+ )
162
+ return role_arn
163
+
164
+ except Exception as e:
165
+ print(f"Error creating IAM role: {e}")
166
+ return None
167
+
168
+
169
+ app = typer.Typer(no_args_is_help=True)
170
+
171
+
172
+ @app.command()
173
+ def main(
174
+ template_path: str = typer.Argument(
175
+ help="Path to the CloudFormation template file"),
176
+ allow_delete: bool = typer.Option(
177
+ False, "-d", "--allow-delete", help="Allow delete permissions instead of denying them"),
178
+ create_role: bool = typer.Option(
179
+ False, "-c", "--create-role", help="Create an IAM role with the generated permissions"),
180
+ role_name: str = typer.Option(
181
+ None, "-r", "--role-name", help="Name for the IAM role (if not specified, uses 'cfn2iam-<random_hash>')"),
182
+ permissions_boundary: str = typer.Option(
183
+ None, "-p", "--permissions-boundary", help="ARN of the permissions boundary to attach to the role"),
184
+ version: Optional[bool] = typer.Option(
185
+ None, "--version", callback=version_callback, help="Show version and exit")
186
+ ):
187
+ """Generate IAM permissions for CloudFormation resources."""
188
+ try:
189
+ print(f"Parsing CloudFormation template: {template_path}")
190
+ resource_types = parse_cloudformation_template(template_path)
191
+
192
+ if not resource_types:
193
+ print("No resource types found in the template.")
194
+ sys.exit(1)
195
+
196
+ all_update_permissions = set()
197
+ all_delete_permissions = set()
198
+
199
+ for resource in resource_types:
200
+ update_permissions, delete_permissions = get_permissions(resource)
201
+ all_update_permissions.update(update_permissions)
202
+ all_delete_permissions.update(delete_permissions)
203
+
204
+ policy_document = generate_policy_document(
205
+ all_update_permissions, all_delete_permissions, allow_delete)
206
+ file_path = f"policy-{generate_random_hash()}.json"
207
+ with open(file_path, 'w') as json_file:
208
+ json.dump(policy_document, json_file, indent=2)
209
+ print(f"\nGenerated IAM Policy Document to {file_path}")
210
+
211
+ if create_role:
212
+ role_name = role_name or f"cfn2iam-{generate_random_hash()}"
213
+
214
+ role_arn = create_iam_role(
215
+ policy_document,
216
+ role_name,
217
+ permissions_boundary
218
+ )
219
+ if role_arn:
220
+ print(f"\nSuccessfully created IAM role: {role_arn}")
221
+
222
+ except Exception as e:
223
+ print(f"Error: {e}")
224
+ sys.exit(1)
225
+
226
+
227
+ if __name__ == "__main__":
228
+ typer.run(main)
cfn2iam-0.1.0/uv.lock ADDED
@@ -0,0 +1,306 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.9"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.10'",
6
+ "python_full_version < '3.10'",
7
+ ]
8
+
9
+ [[package]]
10
+ name = "boto3"
11
+ version = "1.40.7"
12
+ source = { registry = "https://pypi.org/simple" }
13
+ dependencies = [
14
+ { name = "botocore" },
15
+ { name = "jmespath" },
16
+ { name = "s3transfer" },
17
+ ]
18
+ sdist = { url = "https://files.pythonhosted.org/packages/58/5a/f31556d817e872c2723196a34b197d971d78297b22b8bae0ae6d93f7f9c1/boto3-1.40.7.tar.gz", hash = "sha256:61b15f70761f1eadd721c6ba41a92658f003eaaef09500ca7642f5ae68ec8945", size = 111989, upload-time = "2025-08-11T19:20:45.824Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/41/e3/f2a77f4809ffe4e896c2e6186db88333ae980f52a91b28e9fd068d8f5506/boto3-1.40.7-py3-none-any.whl", hash = "sha256:8727cac601a679d2885dc78b8119a0548bbbe04e49b72f7d94021a629154c080", size = 140061, upload-time = "2025-08-11T19:20:43.173Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "botocore"
25
+ version = "1.40.7"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ dependencies = [
28
+ { name = "jmespath" },
29
+ { name = "python-dateutil" },
30
+ { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
31
+ { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
32
+ ]
33
+ sdist = { url = "https://files.pythonhosted.org/packages/73/d7/5e559918410b259c1e54a4646ff39c56433e1c9cefa5e66ab0f06716cee8/botocore-1.40.7.tar.gz", hash = "sha256:33793696680cf3a0c4b5ace4f9070c67c4d4fcb19c999fd85cfee55de3dcf913", size = 14318282, upload-time = "2025-08-11T19:20:33.348Z" }
34
+ wheels = [
35
+ { url = "https://files.pythonhosted.org/packages/b1/fa/bb7ec68b24d1b4678d341a305cbfed78a593e6383c86a70727410e4d0e11/botocore-1.40.7-py3-none-any.whl", hash = "sha256:a06956f3d7222e80ef6ae193608f358c3b7898e1a2b88553479d8f9737fbb03e", size = 13981488, upload-time = "2025-08-11T19:20:27.303Z" },
36
+ ]
37
+
38
+ [[package]]
39
+ name = "cfn2iam"
40
+ source = { editable = "." }
41
+ dependencies = [
42
+ { name = "boto3" },
43
+ { name = "pyyaml" },
44
+ { name = "typer" },
45
+ ]
46
+
47
+ [package.metadata]
48
+ requires-dist = [
49
+ { name = "boto3" },
50
+ { name = "pyyaml" },
51
+ { name = "typer" },
52
+ ]
53
+
54
+ [[package]]
55
+ name = "click"
56
+ version = "8.1.8"
57
+ source = { registry = "https://pypi.org/simple" }
58
+ resolution-markers = [
59
+ "python_full_version < '3.10'",
60
+ ]
61
+ dependencies = [
62
+ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
63
+ ]
64
+ sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
65
+ wheels = [
66
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
67
+ ]
68
+
69
+ [[package]]
70
+ name = "click"
71
+ version = "8.2.1"
72
+ source = { registry = "https://pypi.org/simple" }
73
+ resolution-markers = [
74
+ "python_full_version >= '3.10'",
75
+ ]
76
+ dependencies = [
77
+ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
78
+ ]
79
+ sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
80
+ wheels = [
81
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
82
+ ]
83
+
84
+ [[package]]
85
+ name = "colorama"
86
+ version = "0.4.6"
87
+ source = { registry = "https://pypi.org/simple" }
88
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
89
+ wheels = [
90
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
91
+ ]
92
+
93
+ [[package]]
94
+ name = "jmespath"
95
+ version = "1.0.1"
96
+ source = { registry = "https://pypi.org/simple" }
97
+ sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" }
98
+ wheels = [
99
+ { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
100
+ ]
101
+
102
+ [[package]]
103
+ name = "markdown-it-py"
104
+ version = "3.0.0"
105
+ source = { registry = "https://pypi.org/simple" }
106
+ resolution-markers = [
107
+ "python_full_version < '3.10'",
108
+ ]
109
+ dependencies = [
110
+ { name = "mdurl", marker = "python_full_version < '3.10'" },
111
+ ]
112
+ sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
113
+ wheels = [
114
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
115
+ ]
116
+
117
+ [[package]]
118
+ name = "markdown-it-py"
119
+ version = "4.0.0"
120
+ source = { registry = "https://pypi.org/simple" }
121
+ resolution-markers = [
122
+ "python_full_version >= '3.10'",
123
+ ]
124
+ dependencies = [
125
+ { name = "mdurl", marker = "python_full_version >= '3.10'" },
126
+ ]
127
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
128
+ wheels = [
129
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
130
+ ]
131
+
132
+ [[package]]
133
+ name = "mdurl"
134
+ version = "0.1.2"
135
+ source = { registry = "https://pypi.org/simple" }
136
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
137
+ wheels = [
138
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
139
+ ]
140
+
141
+ [[package]]
142
+ name = "pygments"
143
+ version = "2.19.2"
144
+ source = { registry = "https://pypi.org/simple" }
145
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
146
+ wheels = [
147
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
148
+ ]
149
+
150
+ [[package]]
151
+ name = "python-dateutil"
152
+ version = "2.9.0.post0"
153
+ source = { registry = "https://pypi.org/simple" }
154
+ dependencies = [
155
+ { name = "six" },
156
+ ]
157
+ sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
158
+ wheels = [
159
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
160
+ ]
161
+
162
+ [[package]]
163
+ name = "pyyaml"
164
+ version = "6.0.2"
165
+ source = { registry = "https://pypi.org/simple" }
166
+ sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
167
+ wheels = [
168
+ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
169
+ { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
170
+ { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
171
+ { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
172
+ { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
173
+ { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
174
+ { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
175
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
176
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
177
+ { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
178
+ { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
179
+ { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
180
+ { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
181
+ { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
182
+ { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
183
+ { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
184
+ { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
185
+ { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
186
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
187
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
188
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
189
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
190
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
191
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
192
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
193
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
194
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
195
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
196
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
197
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
198
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
199
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
200
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
201
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
202
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
203
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
204
+ { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" },
205
+ { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" },
206
+ { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" },
207
+ { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" },
208
+ { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" },
209
+ { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" },
210
+ { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" },
211
+ { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" },
212
+ { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" },
213
+ ]
214
+
215
+ [[package]]
216
+ name = "rich"
217
+ version = "14.1.0"
218
+ source = { registry = "https://pypi.org/simple" }
219
+ dependencies = [
220
+ { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
221
+ { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
222
+ { name = "pygments" },
223
+ ]
224
+ sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" }
225
+ wheels = [
226
+ { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" },
227
+ ]
228
+
229
+ [[package]]
230
+ name = "s3transfer"
231
+ version = "0.13.1"
232
+ source = { registry = "https://pypi.org/simple" }
233
+ dependencies = [
234
+ { name = "botocore" },
235
+ ]
236
+ sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload-time = "2025-07-18T19:22:42.31Z" }
237
+ wheels = [
238
+ { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload-time = "2025-07-18T19:22:40.947Z" },
239
+ ]
240
+
241
+ [[package]]
242
+ name = "shellingham"
243
+ version = "1.5.4"
244
+ source = { registry = "https://pypi.org/simple" }
245
+ sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
246
+ wheels = [
247
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
248
+ ]
249
+
250
+ [[package]]
251
+ name = "six"
252
+ version = "1.17.0"
253
+ source = { registry = "https://pypi.org/simple" }
254
+ sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
255
+ wheels = [
256
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
257
+ ]
258
+
259
+ [[package]]
260
+ name = "typer"
261
+ version = "0.16.0"
262
+ source = { registry = "https://pypi.org/simple" }
263
+ dependencies = [
264
+ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
265
+ { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
266
+ { name = "rich" },
267
+ { name = "shellingham" },
268
+ { name = "typing-extensions" },
269
+ ]
270
+ sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" }
271
+ wheels = [
272
+ { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" },
273
+ ]
274
+
275
+ [[package]]
276
+ name = "typing-extensions"
277
+ version = "4.14.1"
278
+ source = { registry = "https://pypi.org/simple" }
279
+ sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
280
+ wheels = [
281
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
282
+ ]
283
+
284
+ [[package]]
285
+ name = "urllib3"
286
+ version = "1.26.20"
287
+ source = { registry = "https://pypi.org/simple" }
288
+ resolution-markers = [
289
+ "python_full_version < '3.10'",
290
+ ]
291
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" }
292
+ wheels = [
293
+ { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" },
294
+ ]
295
+
296
+ [[package]]
297
+ name = "urllib3"
298
+ version = "2.5.0"
299
+ source = { registry = "https://pypi.org/simple" }
300
+ resolution-markers = [
301
+ "python_full_version >= '3.10'",
302
+ ]
303
+ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
304
+ wheels = [
305
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
306
+ ]