cfn2iam 0.1.0__py3-none-any.whl

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,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,7 @@
1
+ source/__init__.py,sha256=Wex3ithyFiSRKcRb4rgt9kyWJfgxH_ABs7NO2jiJhbE,63
2
+ source/app.py,sha256=3LGr4-RDejChF-cm1FwmEHkJn-aQwyJS_jTcsHSSLCg,7378
3
+ cfn2iam-0.1.0.dist-info/METADATA,sha256=xYJ0eioUMy9tdSw5t0q08zRmigyjO45M2BMhX-tjhXw,3617
4
+ cfn2iam-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ cfn2iam-0.1.0.dist-info/entry_points.txt,sha256=OnOURo8kIqzk26E1hWEKetZ27YFAUcPEUu7UfzMF268,43
6
+ cfn2iam-0.1.0.dist-info/licenses/LICENSE,sha256=BX9jvbIeb-ywYBlPQBy6DW8yIYu6RfUoL7xrQ9xbp7o,1072
7
+ cfn2iam-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cfn2iam = source.app:app
@@ -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.
source/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # This file is required to make the directory a Python package
source/app.py ADDED
@@ -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)