diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..776ca1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,122 @@ +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +#blog temp folder +blog + +#mac_quirks +.DS_Store + +#vscode settings +.vscode + +#ignore development versions of iam.json and equivalency_list.json +iam.json +equivalency_list.json + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md old mode 100644 new mode 100755 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100755 index 0000000..26e6199 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.rst +include requirements.txt +include iamctl/conf/* \ No newline at end of file diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100755 index 0000000..5ce980a --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,2 @@ +iamctl +Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 7f92204..0000000 --- a/README.md +++ /dev/null @@ -1,17 +0,0 @@ -## My Project - -TODO: Fill this README out! - -Be sure to: - -* Change the title in this README -* Edit your repository description on GitHub - -## Security - -See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. - -## License - -This library is licensed under the MIT-0 License. See the LICENSE file. - diff --git a/README.rst b/README.rst new file mode 100755 index 0000000..33aa2ae --- /dev/null +++ b/README.rst @@ -0,0 +1,370 @@ +AWS IAM role-comparison tool IAMCTL + +Summary +------- + +IAMCTL is a tool that you can use to extract the IAM roles and policies from two accounts, +compare them, and report out the differences and statistics. We will explain how to use the +tool, and will describe the key concepts so you can configure it to programmatically run +against all of your AWS accounts. + +Prerequisites +------------- + +Before you install the tool and start using it, here are few +prerequisites that need to be in place on the computer where you will +run the tool. + +- `Python 3.x `__ + +- `Git `__ + +- `AWS Command Line Interface + (CLI) `__ + +- Setup AWS CLI profiles for the two accounts you want to compare, as + described in `Named + Profiles `__. + +To follow along in your environment, download the files from the GitHub +repository, and run the steps in order. You will not incur any charges +to run this tool. + +Install the IAMCTL tool +----------------------- + +This section describes how to install and run the IAMCTL tool. + +1. At the command line, enter the following command: + +pip install git+ssh://git@github.com:aws-samples/aws-iamctl.git + +You will see output similar to the following: + +|image0| + +Figure 1: IAMCTL tool installation output + +2. To confirm that your installation was successful, enter the following + command: + +iamctl –h + + You will see results like the following: + +|image1| + +Figure 2: IAMCTL help message + + Now that you have successfully installed the IAMCTL tool, the next + section will show you how to use the IAMCTL commands. + +Run the IAMCTL tool +------------------- + +Initializing IAMCTL +~~~~~~~~~~~~~~~~~~~ + +| To run the IAMCTL tool, initialize using the init command. The init + command does not need any additional input parameters, and uses the + following syntax: +| iamctl init + + **Important**: It is recommended that you run the init command before + you use the IAMCTL tool, and for every subsequent use, to ensure that + the most up-to-date AWS service-specific metadata is available for + the tool for running other commands such as harvest and diff. + + If your init command run is successful, you will see output similar + to the following screenshot. + +|image2| + +Figure 3: Successful initialization output + + The init command creates two files in the directory where you run the + command. First, it downloads iam.json, which contains IAM + service-specific actions, conditions, and the resource ARN regex + available from a `public S3 bucket used by the AWS Policy Generator + tool `__. + + Second, the init command creates equivalency_list.json, which is an + equivalency list JSON file that can be used to store known prefix, + suffix, and other string patterns that are unique to your account and + are considered equal. + + For example, if you have a role named my-app-1-prod in your + production account, and a role named my-app-1-dev in your development + account, you can specify prod and dev strings in the equivalency + dictionary as shown in the following example, so that all occurrences + of those strings will be substituted with accountprefix1. + +|image3| + +Figure 4: Example equivalency list + +Harvest profiles +~~~~~~~~~~~~~~~~ + + The harvest command extracts IAM roles and policies from the AWS + account, as specified in the , and then writes it out to + a CSV file. + + The harvest command also does two additional processing steps. First, + it expands glob patterns in actions to the full list of + service-specific actions. Second, it matches up the resource to a + specific-service action. The CSV file with extracted data has the + format as shown in the following example. + + |image4| + + Figure 5: Example CSV file with extracted data + + We will go into further explanation of each column and row in this + CSV file later in this blog post + + The following screenshot shows the output of the harvest command help + option, which includes the input arguments necessary and a brief + explanation about each option. + +|image5| + +Figure 6: Harvest command help option + +The harvest command uses the following syntax: + +iamctl harvest + + When you run the harvest command, you should see output similar to + the following screenshot. + +|image6| + +Figure 7: Example output from harvest command + + You should now be able to see a file created with name + __iam_tuples.csv in the following directory + path: + +<*user home>*/aws-idt/output/YYYY/mm/dd/HH/MM/SS/. + + The harvest command is useful for use cases where you want to get a + snapshot of all IAM roles and profiles from an AWS account, and + consume it for further processing. Because the extract is flattened + out and expanded, it can be used in reports and analysis, as you + need. + +All files written to disk by the IAMCTL tool are written to the +following location, which includes a time-based directory structure: + +<*user home>*/aws-idt/output/YYYY/mm/dd/HH/MM/SS/ + +The time-based directory structure allows you to periodically run the +**harvest** command, and have it create an archive over time. + +Compare harvested profiles with the diff command +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The diff command compares two accounts to determine differences in IAM +role definitions. The diff command considers a role from each account +for comparison, based on name. The equivalency list populated when you +run the init command is used to ensure that two roles that have known +string patterns in the name that are different between the two accounts +do get picked up for comparison. Comparison results are written out to +disk among multiple files. The naming convention for these files, and +the context of what is written to each of them is explained later in +this blog post. + +The following screenshot shows the output of the diff command help +option, which includes the input arguments necessary and a brief +explanation about each option. + +|image7| + +Figure 8: diff command help option + +The diff command uses the following syntax: + +iamctl diff cli_profile_1 account_tag_1 cli_profile_2 account_tag_2 + +The following screenshot shows the execution of the diff command, along +with the processing status and summary reports of the two profiles. + +|image8| + +Figure 9: diff command execution output + +Interpret the results and find differences +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here is a detailed explanation of what the diff command does, and how to +interpret the results, so that you can find the differences between IAM +roles from the two accounts. + +Step-1: Harvest +'''''''''''''''' + +The diff command relies on the harvest command to extract IAM data from +each of the two accounts. You can see the number of roles in each +account, and the estimated time remaining is indicated in a progress bar +as shown in the previous example. + +Two extract files from this step are written to disk to the output +directory. The following table shows the naming convention, and a brief +description of each. + +============================================== ================================== +File Name Description +============================================== ================================== +__iam_tuples.csv IAM items extracted from Account-1 +__iam_tuples.csv IAM items extracted from Account-2 +============================================== ================================== + +Step-2: Diff +''''''''''''' + +The diff command compares the two extracts generated (one from each +account), then reads in the equivalency list populated from the init +command to reduce false positives. The diff command returns summary +information of all the differences to the output screen, and writes the +full difference information to disk. + +The 19 diff files from this step are written to disk in the output +directory. The following table shows the file naming convention, and a +brief description of each. + +========================================================================== ================================================================================================================== +File Name Description +========================================================================== ================================================================================================================== +_roles.csv List of roles from Account-1. +_non_service_linked_roles.csv List of non service-linked roles from Account-1. +_service_linked_roles.csv List of service-linked roles from Account-1. +_roles.csv List of roles from Account-2. +_non_service_linked_roles.csv List of non service-linked roles from Account-2. +_service_linked_roles.csv List of service-linked roles from Account-2. +common_roles.csv List of common roles between Account-1 and Account-2. A common role is a role with the same name in both accounts. +common_service_linked_roles.csv List of common service-linked roles between Account-1 and Account-2. +common_non_service_linked_roles.csv List of common non service-linked roles between Account-1 and Account-2. +_to__common_role_difference_items.csv List of IAM items from common roles that are in Account-1, but not in Account-2. +_to__common_role_difference_items.csv List of IAM items from common roles that are in Account-2, but not in Account-1. +common_roles_in__with_differences.csv List of IAM roles in Account-1 that are common to both accounts, but have differences. +common_roles_in__with_differences.csv List of IAM roles in Account-2 that are common to both accounts, but have differences. +roles_in__but_not_in_.csv List of IAM roles that are unique to Account-1. +roles_in__but_not_in_.csv List of IAM roles that are unique to Account-2. +service_linked_roles_in__but_not_in_.csv List of service-linked IAM roles that are unique to Account-1. +service_linked_roles_in__but_not_in_.csv List of service-linked IAM roles that are unique to Account-2. +non_service_linked_roles_in__but_not_in_.csv List of non service-linked IAM roles that are unique to Account-1. +non_service_linked_roles_in__but_not_in_.csv List of non service-linked IAM roles that are unique to Account-2. +========================================================================== ================================================================================================================== + +How to interpret the output tables +----------------------------------- + +Both the harvest and diff commands write out *IAM items*. An IAM item is +each row of a two-dimensional table that results from flattening an IAM +role into its constituent components, and mapping each attribute to a +column of that table, as shown in the following diagram. An IAM role can +have multiple policies associated with it. For more information, see the +`IAM User +Guide `__. + +|image9| + +Figure 10: Example of IAM Role with various policies flattened into IAM +items + +The following table shows attributes of an IAM Item and a brief +definition of each. + +=============== ==================================================================================== +Attribute name Definition +=============== ==================================================================================== +Role Name Name of the IAM role. +Policy Name of the IAM policy. +Policy Type One of the values: “Inline”, “Managed”, “Trust” +Effect IAM Statement effect: “Allow”, “Deny” +Service Service name +Action Action within the service +Trust Principal Principal that is trusted to assume this role, populated only for Policy Type Trust. +=============== ==================================================================================== + +The following screenshot shows the console output for the diff command +run against two example accounts. + +|image10| + +Figure 10: Example output for the diff command run against two accounts + +The following table provides an explanation for each metric from the +summary report in tabular format in the previous example. + +=============================== ====================================================================================================================================== +Metric Definition +=============================== ====================================================================================================================================== +Harvested items Count of *IAM item*\ s. See earlier for detailed explanation of an *IAM Item*. +Sanitized items Count of *IAM item*\ s after applying the equivalency dictionary. +Roles Count of IAM roles. +Service linked roles Count of IAM roles with a “/aws-service-role/” path. +Non-Service linked roles Count of IAM roles without a “/aws-service-role/” path. +Common roles Count of IAM roles that are similar by name from both accounts, specified as parameters for diff. +Common service linked roles Count of IAM roles with a “/aws-service-role/” path, that are similar by name from both accounts, specified as parameters for diff. +Common Non-Service linked roles Count of IAM roles without a “/aws-service-role/” path, that are similar by name from both accounts, specified as parameters for diff. +Unique roles Count of IAM roles that exist only in that account, based on name as compared to the other account. +Unique service linked roles Count of service-linked roles that exist only in that account, based on name as compared to the other account. +Unique non-service linked roles Count of non-service-linked roles that exist only in that account, based on name as compared to the other account. +Common roles with differences Count of roles that are common to both accounts, based on name, but have differences as seen in any of the *IAM item*\ s. +Differences among common roles Count of *IAM items* among common roles that are different, as seen from this account compared to the other account. +=============================== ====================================================================================================================================== + +The diff command output presents both summarized statistics and granular +lists, which allow you to see the number of deviations between two +accounts, and also provide actionable output to help you remediate these +differences. + +Conclusion: +----------- + +You have learnt how to use the IAMCTL tool to compare IAM +roles between two accounts, to arrive at a granular list of meaningful +differences that can be used for compliance audits or for further +remediation actions. If you have created your IAM roles using a +CloudFormation stack, you can turn on drift detection and easily capture +the drift because of changes done outside of CloudFormation to those IAM +resources. For more information about drift detection, see `Detecting +Unmanaged Configuration Changes to Stacks and +Resources `__. +we welcome any pull requests for issues and enhancements. + +.. |image0| image:: .//media/image1.png + :width: 6.5in + :height: 1.01528in +.. |image1| image:: .//media/image2.png + :width: 5.86368in + :height: 6.29878in +.. |image2| image:: .//media/image3.png + :width: 6.5in + :height: 2.39028in +.. |image3| image:: .//media/image4.png + :width: 5.26153in + :height: 3.55088in +.. |image4| image:: .//media/image5.png + :width: 6.5in + :height: 0.61319in +.. |image5| image:: .//media/image6.png + :width: 6.5in + :height: 3.03542in +.. |image6| image:: .//media/image7.png + :width: 6.5in + :height: 1.5848in +.. |image7| image:: .//media/image8.png + :width: 6.5in + :height: 3.40417in +.. |image8| image:: .//media/image9.png + :width: 6.5in + :height: 2.07917in +.. |image9| image:: .//media/image10.png + :width: 6.5in + :height: 2.8296in +.. |image10| image:: .//media/image11.png + :width: 5.48611in + :height: 9in diff --git a/THIRD-PARTY.txt b/THIRD-PARTY.txt new file mode 100755 index 0000000..a456389 --- /dev/null +++ b/THIRD-PARTY.txt @@ -0,0 +1,418 @@ +** boto3; version 1.9.237 -- https://pypi.org/project/boto3/ +boto3 +Copyright 2013-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +** jmespath; version 1.3.1 -- http://github.com/trevorrowe/jmespath.rb +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ +** s3transfer; version 0.2.0 -- https://pypi.org/project/s3transfer/ +Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND +DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, and + distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by the + copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all other + entities that control, are controlled by, or are under common control + with that entity. For the purposes of this definition, "control" means + (i) the power, direct or indirect, to cause the direction or management + of such entity, whether by contract or otherwise, or (ii) ownership of + fifty percent (50%) or more of the outstanding shares, or (iii) + beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity exercising + permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation source, + and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but not limited + to compiled object code, generated documentation, and conversions to + other media types. + + "Work" shall mean the work of authorship, whether in Source or Object + form, made available under the License, as indicated by a copyright + notice that is included in or attached to the work (an example is + provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object form, + that is based on (or derived from) the Work and for which the editorial + revisions, annotations, elaborations, or other modifications represent, + as a whole, an original work of authorship. For the purposes of this + License, Derivative Works shall not include works that remain separable + from, or merely link (or bind by name) to the interfaces of, the Work and + Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including the original + version of the Work and any modifications or additions to that Work or + Derivative Works thereof, that is intentionally submitted to Licensor for + inclusion in the Work by the copyright owner or by an individual or Legal + Entity authorized to submit on behalf of the copyright owner. For the + purposes of this definition, "submitted" means any form of electronic, + verbal, or written communication sent to the Licensor or its + representatives, including but not limited to communication on electronic + mailing lists, source code control systems, and issue tracking systems + that are managed by, or on behalf of, the Licensor for the purpose of + discussing and improving the Work, but excluding communication that is + conspicuously marked or otherwise designated in writing by the copyright + owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity on + behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of this + License, each Contributor hereby grants to You a perpetual, worldwide, + non-exclusive, no-charge, royalty-free, irrevocable copyright license to + reproduce, prepare Derivative Works of, publicly display, publicly perform, + sublicense, and distribute the Work and such Derivative Works in Source or + Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of this + License, each Contributor hereby grants to You a perpetual, worldwide, + non-exclusive, no-charge, royalty-free, irrevocable (except as stated in + this section) patent license to make, have made, use, offer to sell, sell, + import, and otherwise transfer the Work, where such license applies only to + those patent claims licensable by such Contributor that are necessarily + infringed by their Contribution(s) alone or by combination of their + Contribution(s) with the Work to which such Contribution(s) was submitted. + If You institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work or a + Contribution incorporated within the Work constitutes direct or contributory + patent infringement, then any patent licenses granted to You under this + License for that Work shall terminate as of the date such litigation is + filed. + + 4. Redistribution. You may reproduce and distribute copies of the Work or + Derivative Works thereof in any medium, with or without modifications, and + in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a + copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating + that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You + distribute, all copyright, patent, trademark, and attribution notices + from the Source form of the Work, excluding those notices that do not + pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must include + a readable copy of the attribution notices contained within such NOTICE + file, excluding those notices that do not pertain to any part of the + Derivative Works, in at least one of the following places: within a + NOTICE text file distributed as part of the Derivative Works; within the + Source form or documentation, if provided along with the Derivative + Works; or, within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents of the + NOTICE file are for informational purposes only and do not modify the + License. You may add Your own attribution notices within Derivative Works + that You distribute, alongside or as an addendum to the NOTICE text from + the Work, provided that such additional attribution notices cannot be + construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may + provide additional or different license terms and conditions for use, + reproduction, or distribution of Your modifications, or for any such + Derivative Works as a whole, provided Your use, reproduction, and + distribution of the Work otherwise complies with the conditions stated in + this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, any + Contribution intentionally submitted for inclusion in the Work by You to the + Licensor shall be under the terms and conditions of this License, without + any additional terms or conditions. Notwithstanding the above, nothing + herein shall supersede or modify the terms of any separate license agreement + you may have executed with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, except + as required for reasonable and customary use in describing the origin of the + Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in + writing, Licensor provides the Work (and each Contributor provides its + Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied, including, without limitation, any + warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or + FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining + the appropriateness of using or redistributing the Work and assume any risks + associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, whether + in tort (including negligence), contract, or otherwise, unless required by + applicable law (such as deliberate and grossly negligent acts) or agreed to + in writing, shall any Contributor be liable to You for damages, including + any direct, indirect, special, incidental, or consequential damages of any + character arising as a result of this License or out of the use or inability + to use the Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all other + commercial damages or losses), even if such Contributor has been advised of + the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing the Work + or Derivative Works thereof, You may choose to offer, and charge a fee for, + acceptance of support, warranty, indemnity, or other liability obligations + and/or rights consistent with this License. However, in accepting such + obligations, You may act only on Your own behalf and on Your sole + responsibility, not on behalf of any other Contributor, and only if You + agree to indemnify, defend, and hold each Contributor harmless for any + liability incurred by, or claims asserted against, such Contributor by + reason of your accepting any such warranty or additional liability. END OF + TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification +within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. + +* For boto3 see also this required NOTICE: + boto3 + Copyright 2013-2017 Amazon.com, Inc. or its affiliates. All Rights + Reserved. +* For jmespath see also this required NOTICE: + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ +* For s3transfer see also this required NOTICE: + Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +------ + +** colorama; version 0.3.9 -- https://github.com/tartley/colorama +Copyright (c) 2010 Jonathan Hartley +All rights reserved. + +Copyright (c) 2010 Jonathan Hartley +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holders, nor those of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------ + +** pyfiglet; version 0.8.post1 -- https://github.com/pwaller/pyfiglet +Copyright © 2007-2018 + Christopher Jones + Stefano Rivera + Peter Waller + And various contributors (see git history) + +PyFiglet: An implementation of figlet written in Python + +The MIT License (MIT) + +Copyright © 2007-2018 + Christopher Jones + Stefano Rivera + Peter Waller + And various contributors (see git history). + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +------ + +** urllib3; version 1.24.3 -- https://pypi.python.org/pypi/urllib3/1.22 +Copyright 2008-2016 Andrey Petrov and contributors (see CONTRIBUTORS.txt) + +https://github.com/shazow/urllib3/blob/master/CONTRIBUTORS.txt + +MIT License + +Copyright (c) 2008-2019 Andrey Petrov and contributors (see CONTRIBUTORS.txt) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------ + +** six; version 1.12.0 -- https://pypi.python.org/pypi/six/1.11.0 +Copyright (c) 2010-2017 Benjamin Peterson + +Copyright (c) 2010-2017 Benjamin Peterson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------ + +** terminaltables; version 3.1.0 -- https://github.com/Robpol86/terminaltables +Copyright (c) 2017 Robpol86 + +MIT License + +Copyright (c) 2017 Robpol86 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------ + +** ProgressBar; version 1.5 -- https://github.com/kimmobrunfeldt/progressbar.js +The MIT License (MIT) + +Copyright (c) 2015 Kimmo Brunfeldt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +The MIT License (MIT) + +Copyright (c) 2015 Kimmo Brunfeldt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/bin/iamctl b/bin/iamctl new file mode 100755 index 0000000..b827428 --- /dev/null +++ b/bin/iamctl @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +import iamctl.iamctl +import sys + + +def main(): + return iamctl.iamctl.main() + + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/iamctl/__init__.py b/iamctl/__init__.py new file mode 100755 index 0000000..a1fe6b0 --- /dev/null +++ b/iamctl/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +from colorama import init + +init() \ No newline at end of file diff --git a/iamctl/__main__.py b/iamctl/__main__.py new file mode 100755 index 0000000..8616148 --- /dev/null +++ b/iamctl/__main__.py @@ -0,0 +1,20 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +import sys + +from iamctl.iamctl import main + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/iamctl/conf/logging.conf b/iamctl/conf/logging.conf new file mode 100755 index 0000000..46a9f85 --- /dev/null +++ b/iamctl/conf/logging.conf @@ -0,0 +1,34 @@ +[loggers] +keys=root,harvester,differ + +[handlers] +keys=consoleHandler + +[formatters] +keys=simpleFormatter + +[logger_root] +level=ERROR +handlers=consoleHandler + +[logger_harvester] +level=ERROR +handlers=consoleHandler +qualname=harvester +propagate=0 + +[logger_differ] +level=ERROR +handlers=consoleHandler +qualname=differ +propagate=0 + +[handler_consoleHandler] +class=StreamHandler +level=ERROR +formatter=simpleFormatter +args=(sys.stdout,) + +[formatter_simpleFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt= \ No newline at end of file diff --git a/iamctl/differ.py b/iamctl/differ.py new file mode 100755 index 0000000..18962da --- /dev/null +++ b/iamctl/differ.py @@ -0,0 +1,280 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +import boto3 +import json +from botocore.exceptions import ClientError +import re +import logging +import logging.config +import csv +import sys +import os +import argparse +import time +from datetime import datetime +from progress.bar import ChargingBar, Bar +from pyfiglet import Figlet +from colorama import init,Fore, Back, Style +from terminaltables import SingleTable +from os.path import expanduser +from os import path + +class Differ: + def __init__(self, extract_file_name_1, extract_file_name_2, account_1_tag, account_2_tag, output_directory): + + self.output_directory=output_directory + self.logger = logging.getLogger(__name__) + self.extract_file_name_1 = extract_file_name_1 + self.extract_file_name_2 = extract_file_name_2 + self.extract_file_handler_1 = None + self.extract_file_handler_2 = None + self.account_1_tag = account_1_tag + self.account_2_tag = account_2_tag + self.account_1_raw = None + self.account_2_raw = None + self.equivalency_list_dict = None + self.account_1_to_account_2_csv_out = None + self.account_2_to_account_1_csv_out = None + self.read_equivalency_dict() + self.read_extract_files() + + def read_equivalency_dict(self): + with open('equivalency_list.json') as f: + self.equivalency_list_dict = json.load(f) + + def read_extract_files(self): + # Read the 2 extract files. + with open(self.extract_file_name_1) as f: + self.extract_file_handler_1 = f + next(csv.reader(f)) + self.account_1_raw = [tuple(line) for line in csv.reader(f)] + + with open(self.extract_file_name_2) as f: + self.extract_file_handler_2 = f + next(csv.reader(f)) + self.account_2_raw = [tuple(line) for line in csv.reader(f)] + + # Get values after matching equivalency list + def sanitize_value_with_equivalency(self, value): + for key, valuelist in self.equivalency_list_dict.items(): + for eachvalue in valuelist: + value = value.replace(eachvalue, key) + return value + + # Get list after matching equivalency list. + def get_sanitized_list_with_equivalency(self, tuples, tag): + output_list = [] + bar = ChargingBar('Sanitizing IAM items from '+tag, max=len(tuples),suffix='%(index)d/%(max)d') + for each_tuple in tuples: + output_tuple = [] + for each_item in each_tuple: + output_tuple.append(self.sanitize_value_with_equivalency(each_item)) + output_list.append(tuple(output_tuple)) + bar.next() + bar.finish() + return output_list + + def write_to_csv(self,tuples, header, filename): + filehandler = open(self.output_directory+ "/" + filename, "wt", newline='') + csv_out = csv.writer(filehandler) + csv_out.writerow(header) + for each_tuple in tuples: + csv_out.writerow(each_tuple) + filename = filehandler.name + filehandler.close() + return filename + + def generate_diff_and_summary(self): + + summary = [] + summary.append(['Metric', self.account_1_tag, self.account_2_tag]) + + sanitized_account_1_list = self.get_sanitized_list_with_equivalency(self.account_1_raw, self.account_1_tag) + sanitized_account_2_list = self.get_sanitized_list_with_equivalency(self.account_2_raw, self.account_2_tag) + + print(Style.BRIGHT) + print(Fore.BLUE + "Summary report in text format:") + print(Style.RESET_ALL) + + print("Number of items in %s: %d" %(self.account_1_tag, len(self.account_1_raw))) + print("Number of items in %s: %d" %(self.account_2_tag, len(self.account_2_raw))) + + summary.append(['Harvested Items', len(self.account_1_raw), len(self.account_2_raw)]) + + print("Number of items in %s after sanitizing: %d" % (self.account_1_tag, len(sanitized_account_1_list))) + print("Number of items in %s after sanitizing: %d" % (self.account_2_tag, len(sanitized_account_2_list))) + + summary.append(['Sanitized Items', len(sanitized_account_1_list), len(sanitized_account_2_list)]) + + account_1_roles = set([(item[0], item[1]) for item in sanitized_account_1_list]) + print("Number of roles in %s: %d" %(self.account_1_tag, len(account_1_roles))) + headerrow =('rolename', 'path') + filename = self.account_1_tag + "_roles.csv" + self.write_to_csv(account_1_roles, headerrow, filename) + + account_1_service_linked_roles = set([(item[0],) for item in sanitized_account_1_list if (item[1].startswith('/aws-service-role/'))]) + print("Number of service linked roles in %s: %d" %(self.account_1_tag, len(account_1_service_linked_roles))) + headerrow =('rolename',) + filename = self.account_1_tag + "_service_linked_roles.csv" + self.write_to_csv(account_1_service_linked_roles, headerrow, filename) + + account_1_non_service_linked_roles = set([(item[0],) for item in sanitized_account_1_list if not item[1].startswith('/aws-service-role/')]) + print("Number of Non-service linked roles in %s: %d" %(self.account_1_tag, len(account_1_non_service_linked_roles))) + headerrow =('rolename',) + filename = self.account_1_tag + "_non_service_linked_roles.csv" + self.write_to_csv(account_1_non_service_linked_roles, headerrow, filename) + + account_2_roles = set([(item[0],item[1]) for item in sanitized_account_2_list]) + print("Number of roles in %s: %d" %(self.account_2_tag, len(account_2_roles))) + headerrow =('rolename', 'path') + filename = self.account_2_tag + "_roles.csv" + self.write_to_csv(account_2_roles, headerrow, filename) + + account_2_service_linked_roles = set([(item[0],) for item in sanitized_account_2_list if (item[1].startswith('/aws-service-role/'))]) + print("Number of service linked roles in %s: %d" %(self.account_2_tag, len(account_2_service_linked_roles))) + headerrow =('rolename',) + filename = self.account_2_tag + "_service_linked_roles.csv" + self.write_to_csv(account_2_service_linked_roles, headerrow, filename) + + account_2_non_service_linked_roles = set([(item[0],) for item in sanitized_account_2_list if not item[1].startswith('/aws-service-role/')]) + print("Number of Non-service linked roles in %s: %d" %(self.account_2_tag, len(account_2_non_service_linked_roles))) + headerrow =('rolename',) + filename = self.account_2_tag + "_non_service_linked_roles.csv" + self.write_to_csv(account_2_non_service_linked_roles, headerrow, filename) + + summary.append(['Roles',len(account_1_roles), len(account_2_roles)]) + summary.append(['Service Linked Roles', len(account_1_service_linked_roles), len(account_2_service_linked_roles)]) + summary.append(['Non-Service Linked Roles', len(account_1_non_service_linked_roles), len(account_2_non_service_linked_roles)]) + + # Getting list of common roles between 2 accounts using bitwise operator. + # If roles are common will return a 1. + common_role_list = set(account_1_roles) & set(account_2_roles) + print("Number of common roles: %d" %( len(common_role_list))) + headerrow =('rolename', 'path') + filename = "common_roles.csv" + self.write_to_csv(common_role_list, headerrow, filename) + + common_service_linked_role_list = [(item[0],) for item in common_role_list if item[1].startswith('/aws-service-role/')] + print("Number of common roles that are service linked: %d" %(len(common_service_linked_role_list))) + headerrow =('rolename',) + filename = "common_service_linked_roles.csv" + self.write_to_csv(common_service_linked_role_list, headerrow, filename) + + common_non_service_linked_role_list = [(item[0],) for item in common_role_list if not item[1].startswith('/aws-service-role/')] + print("Number of common roles that are Non-service linked: %d" %(len(common_non_service_linked_role_list))) + headerrow =('rolename',) + filename = "common_non_service_linked_roles.csv" + self.write_to_csv(common_non_service_linked_role_list, headerrow, filename) + + summary.append(['Common Roles', len(common_role_list), len(common_role_list)]) + summary.append(['Common Service Linked Roles', len(common_service_linked_role_list), len(common_service_linked_role_list)]) + summary.append(['Common Non-Service Linked Roles', len(common_non_service_linked_role_list), len(common_non_service_linked_role_list)]) + + # Get roles that are in first but not in second. + account_1_diff_account_2 = set(account_1_roles) - set(account_2_roles) + + + #difference in items will not translate to roles, for e.g. you could have a role in account-a that has action item that is not in account-b + account_1_diff_account_2_roles = set([(item[0],item[1]) for item in account_1_diff_account_2]) + print("Number of roles from %s not in %s: %d" %(self.account_1_tag,self.account_2_tag, len(account_1_diff_account_2_roles))) + headerrow =('rolename', 'path') + filename = "roles_in_" + self.account_1_tag + "_but_not_in_" + self.account_2_tag + ".csv" + self.write_to_csv(account_1_diff_account_2_roles, headerrow, filename) + + account_1_diff_account_2_service_linked_roles= [tup for tup in account_1_diff_account_2_roles if tup[1].startswith('/aws-service-role/')] + print("Number of service linked roles from %s not in %s: %d" %(self.account_1_tag,self.account_2_tag, len(account_1_diff_account_2_service_linked_roles))) + headerrow =('rolename', 'path') + filename = "service_linked_roles_in_" + self.account_1_tag + "_but_not_in_" + self.account_2_tag+" .csv" + self.write_to_csv(account_1_diff_account_2_service_linked_roles, headerrow, filename) + + account_1_diff_account_2_non_service_linked_roles= [tup for tup in account_1_diff_account_2_roles if not tup[1].startswith('/aws-service-role/')] + print("Number of non-service linked roles from %s not in %s: %d" %(self.account_1_tag, self.account_2_tag, len(account_1_diff_account_2_non_service_linked_roles))) + headerrow =('rolename', 'path') + filename = "non_service_linked_roles_in_" + self.account_1_tag + "_but_not_in_" + self.account_2_tag + ".csv" + self.write_to_csv(account_1_diff_account_2_non_service_linked_roles, headerrow, filename) + + # Get roles that are in second but not in first. + + account_2_diff_account_1 = set(account_2_roles) - set(account_1_roles) + account_2_diff_account_1_roles = list(set([(item[0],item[1]) for item in account_2_diff_account_1])) + print("Number of roles from %s not in %s: %d" %(self.account_2_tag,self.account_1_tag, len(account_2_diff_account_1_roles))) + headerrow =('rolename', 'path') + filename = "roles_in_" + self.account_2_tag + "_but_not_in_" + self.account_2_tag + ".csv" + self.write_to_csv(account_2_diff_account_1_roles, headerrow, filename) + + account_2_diff_account_1_service_linked_roles= [tup for tup in account_2_diff_account_1_roles if tup[1].startswith('/aws-service-role/')] + print("Number of service linked roles from %s not in %s: %d" %(self.account_2_tag,self.account_1_tag, len(account_2_diff_account_1_service_linked_roles))) + headerrow =('rolename', 'path') + filename = "service_linked_roles_in_" + self.account_2_tag + "_but_not_in_" + self.account_1_tag + ".csv" + self.write_to_csv(account_2_diff_account_1_service_linked_roles, headerrow, filename) + + + account_2_diff_account_1_non_service_linked_roles= [tup for tup in account_2_diff_account_1_roles if not tup[1].startswith('/aws-service-role/')] + print("Number of non-service linked roles from %s not in %s: %d" %(self.account_2_tag, self.account_1_tag, len(account_2_diff_account_1_non_service_linked_roles))) + headerrow =('rolename', 'path') + filename = "non_service_linked_roles_in_" + self.account_2_tag + "_but_not_in_" + self.account_1_tag + ".csv" + self.write_to_csv(account_2_diff_account_1_non_service_linked_roles, headerrow, filename) + + summary.append(['Unique Roles', len(account_1_diff_account_2_roles), len(account_2_diff_account_1_roles)]) + summary.append(['Unique Service Linked Roles', len(account_1_diff_account_2_service_linked_roles), len(account_2_diff_account_1_service_linked_roles)]) + summary.append(['Unique Non-Service Linked Roles', len(account_1_diff_account_2_non_service_linked_roles), len(account_2_diff_account_1_non_service_linked_roles)]) + + account_1_diff_items_account_2 = set(sanitized_account_1_list).difference(set(sanitized_account_2_list)) + account_2_diff_items_account_1 = set(sanitized_account_2_list).difference(set(sanitized_account_1_list)) + + + true_diff_account_1_with_common = [tup for tup in account_1_diff_items_account_2 if (tup[0] in [item[0] for item in common_role_list])] + print("There are %d items that are in different in %s among common roles between %s,%s" %(len(true_diff_account_1_with_common),self.account_1_tag, self.account_1_tag,self.account_2_tag)) + headerrow =('rolename', 'path','trust', 'policyname', 'effect', 'service', 'action', 'arn') + filename = self.account_1_tag + "_to_" + self.account_2_tag + "_common_role_difference_items.csv" + self.write_to_csv(true_diff_account_1_with_common, headerrow, filename) + + true_diff_role_account_1_with_common = set([(item[0],) for item in true_diff_account_1_with_common]) + print("There are %d common roles in %s that have differences with %s "%(len(true_diff_role_account_1_with_common), self.account_1_tag, self.account_2_tag)) + headerrow =('rolename',) + filename = "common_roles_in_" + self.account_1_tag + "_with_differences" + ".csv" + self.write_to_csv(true_diff_role_account_1_with_common, headerrow, filename) + + # base_diff_account_2_with_common = [tup for tup in account_2 if (tup[0] in common_role_list)] + # print("Number of items in " + account2tag + " tied to common roles: " + str(len(base_diff_account_2_with_common))) + true_diff_account_2_with_common = [tup for tup in account_2_diff_items_account_1 if (tup[0] in [item[0] for item in common_role_list])] + print("There are %d items that are in different in %s among common roles between %s,%s" %(len(true_diff_account_2_with_common), self.account_2_tag, self.account_1_tag, self.account_2_tag)) + headerrow =('rolename', 'path','trust', 'policyname', 'effect', 'service', 'action', 'arn') + filename = self.account_2_tag + "_to_" + self.account_1_tag + "_common_role_difference_items.csv" + self.write_to_csv(true_diff_account_2_with_common, headerrow, filename) + + true_diff_role_account_2_with_common = set([(item[0],) for item in true_diff_account_2_with_common]) + print("There are %d common roles in %s that have differences with %s "%(len(true_diff_role_account_2_with_common), self.account_2_tag, self.account_1_tag)) + headerrow = ('rolename',) + filename = "common_roles_in_" + self.account_2_tag + "_with_differences" + ".csv" + self.write_to_csv(true_diff_role_account_2_with_common, headerrow, filename) + + summary.append(['Common Roles with Differences', len(true_diff_role_account_1_with_common), len(true_diff_role_account_2_with_common)]) + summary.append(['Differences among Common Roles', len(true_diff_account_1_with_common), len(true_diff_account_2_with_common)]) + + print(Style.BRIGHT) + print(Fore.YELLOW +"Summary report in tabular format:") + print(Style.RESET_ALL) + + table = SingleTable(summary) + table.title = "Summary Report" + table.inner_heading_row_border = True + table.inner_row_border = True + table.justify_columns[1] = 'right' + table.justify_columns[2] = 'right' + print(table.table) + + print(Style.BRIGHT) + print(Fore.GREEN + "Detailed reports are available at this location:\n%s" %(self.output_directory)) + print(Style.RESET_ALL) diff --git a/iamctl/harvester.py b/iamctl/harvester.py new file mode 100755 index 0000000..ce1375b --- /dev/null +++ b/iamctl/harvester.py @@ -0,0 +1,339 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +import boto3 +import json +from botocore.exceptions import ClientError +import re +import fnmatch +import logging +import logging.config +import csv +import sys +import os +import argparse +import time +from datetime import datetime +from progress.bar import ChargingBar, Bar +from pyfiglet import Figlet +from colorama import init,Fore, Back, Style +from terminaltables import SingleTable +from os.path import expanduser +from os import path + +class Harvester: + + def close_file_handler(self): + self.extract_file.close() + + def read_iam_file(self): + with open('iam.json') as json_file: + return json.load(json_file) + + def return_service_iam_actions(self,service_prefix): + for p in self.iam_reference['serviceMap']: + if (self.iam_reference['serviceMap'][p]['StringPrefix'] == service_prefix): + return self.iam_reference['serviceMap'][p]['Actions'] + + def return_service_arns(self): + arns=[] + for p in self.iam_reference['serviceMap']: + if ('ARNRegex' in self.iam_reference['serviceMap'][p]): + arns.append({'ARNRegex':self.iam_reference['serviceMap'][p]['ARNRegex'], 'StringPrefix':self.iam_reference['serviceMap'][p]['StringPrefix']}) + return arns + + def match_action_regex(self, match_action, service_prefix): + matches = [] + actions = self.return_service_iam_actions(service_prefix) + for action in actions or []: + if fnmatch.fnmatch(action, match_action): + matches.append(action) + return matches + + + def match_resource_regex(self, match_resource): + matches = [] + arns = self.return_service_arns() + for arn in arns or []: + arn_regex = re.compile(arn['ARNRegex']) + if arn_regex.match(match_resource): + matches.append(arn) + return matches + + + def get_iam_roles(self): + paginator = self.client.get_paginator('list_roles') + response_iterator = paginator.paginate( + PaginationConfig = { + 'PageSize': 1000, + 'StartingToken': None}) + + roles = response_iterator.build_full_result() + self.logger.info("Number of roles: %d",len(roles['Roles'])) + return roles['Roles'] + + + def get_role_inline_policies(self, role_name): + return self.client.list_role_policies( + RoleName = role_name + ) + + + def get_role_attached_policies(self, role_name): + + return self.client.list_attached_role_policies( + RoleName = role_name + ) + + def get_policy(self, policy_arn): + return self.client.get_policy(PolicyArn = policy_arn) + + def get_policy_version(self,policy_arn, version_id): + return self.client.get_policy_version(PolicyArn = policy_arn, VersionId = version_id) + + def get_role_policy(self, rolename, inline_policy_name): + return self.client.get_role_policy(RoleName = rolename, PolicyName = inline_policy_name) + + def get_role(self, role_name): + return self.client.get_role(RoleName = role_name) + + def parse_statement_action(self,action_tag ,statement_action): + actions = [] + + if(statement_action == "*"): + self.logger.info("All Actions against all Services") + actions.append({'service':'*' , action_tag:'*'}) + else: + self.logger.debug("Statement Action: " + statement_action) + self.logger.debug(action_tag+": " + statement_action.encode("utf-8").decode().split(':')[1]) + self.logger.debug("service: " + statement_action.encode("utf-8").decode().split(':')[0]) + action_matches = self.match_action_regex(statement_action.encode("utf-8").decode().split(':')[1], statement_action.encode("utf-8").decode().split(':')[0]) + for action in action_matches or []: + actions.append({'service' : statement_action.encode("utf-8").decode().split(':')[0], action_tag:action}) + self.logger.info("Statement Action: " + statement_action.encode("utf-8").decode().split(':')[0]+" : " + action ) + return actions + + def parse_statement_resource(self,resource_tag, statement_resource): + resources = [] + if(statement_resource == "*"): + self.logger.info("All resources for all Services") + resources.append({'service' : '*' , resource_tag : '*'}) + else: + resource_matches = self.match_resource_regex(statement_resource) + for resource in resource_matches: + resources.append({'service' : resource['StringPrefix'], resource_tag : statement_resource}) + self.logger.info("Statement Resource: " + resource['StringPrefix'] + " : " + statement_resource) + + return resources + + def mux(self,action_tag,actions,resource_tag,resources): + #actions structure is: service, action + #resources sturcture is: service, arn + #muxedup structure is: service, action, arn + self.logger.debug("I am muxed up and I received this actions:") + self.logger.debug(str(actions)) + self.logger.debug("I am muxed up and I received this resources:") + self.logger.debug(str(resources)) + muxedup=[] + for action in actions: + for resource in resources: + if ((action['service'] == resource['service']) or (action['service'] == "*") or (resource['service'] == "*")): + muxedup.append({'service': action['service'], 'action' : action[action_tag], 'arn' : resource[resource_tag]}) + + return muxedup + + + def parse_policy(self,policy_document): + # instantiate empty policy array and policy statement array + policy_statement_array = [] + parsed_policy = [] + + # determining if there is a single statement or an array of statements in the policy document + # and appending those statement(s) to policy_statement_array + # + if not isinstance(policy_document['Statement'], list): + policy_statement_array.append(policy_document['Statement']) + else: + policy_statement_array = policy_document['Statement'] + + # code that parses each policy statement into its components + # and calls parse_statement_action for action/notaction, parse_statement_resource for resource/notresource block + for policy_statement in policy_statement_array: + self.logger.info("Statement Effect: "+policy_statement['Effect']) + actions = [] + statement_has_action = 'Action' + # Checking if statement has action or notaction block + if policy_statement.get('Action',False): + statement_has_action = 'Action' + else: + statement_has_action = 'NotAction' + # checking if Action is single item or a list + if not isinstance(policy_statement[statement_has_action], list): + actions=actions + self.parse_statement_action(statement_has_action, policy_statement[statement_has_action]) + else: + for statement_action in policy_statement[statement_has_action]: + actions = actions+self.parse_statement_action(statement_has_action, statement_action) + + + + resources=[] + statement_has_resource = 'Resource' + # Checking if statment has resource or notresource block + if policy_statement.get('Resource',False): + statement_has_resource = 'Resource' + else: + statement_has_resource = 'NotResource' + self.logger.debug("Statement Resource: "+str(policy_statement[statement_has_resource])) + if not isinstance(policy_statement[statement_has_resource], list): + resources=resources+self.parse_statement_resource(statement_has_resource, policy_statement[statement_has_resource]) + else: + for statement_resource in policy_statement[statement_has_resource]: + resources = resources + self.parse_statement_resource(statement_has_resource, statement_resource) + + muxed_up=self.mux(statement_has_action,actions,statement_has_resource,resources) + self.logger.debug("Going to print Muxed up results for: ") + self.logger.debug(str(muxed_up)) + parsed_policy.append({'effect' : policy_statement['Effect'], 'action_resources' : muxed_up }) + return parsed_policy + + def write_out_exhaust(self, role): + + #self.logger.info("here is the exhaust",str(exhaust)) + #exhaust: data: List + #role: rolename:string,trust:string,parsed_policies: List + #policy: policyname: string, parsed_statements: List + #statement: effect: string, action_resources: List + #action_resource: service, action, arn + #Final write out (vsad, somesuffix, trust, policyname, effect, service, action, resource) + #MVP write out (rolename, trust, policyname, effect, service, action, resource) + csv_out = self.csv_out + + + for policy in role['policies']: + self.logger.info("here is the policy",str(policy)) + if policy['type'] == "trust": + for statement in policy['statements']: + csv_out.writerow((role['name'],role['path'],policy['name'],policy['type'], statement['effect'],statement['service'], statement['action'], None ,statement['principal'])) + + else: + for statement in policy['statements']: + for action_resource in statement['action_resources']: + csv_out.writerow((role['name'], role['path'], policy['name'], policy['type'], statement['effect'], action_resource['service'], action_resource['action'], action_resource['arn'], None)) + + def get_role_trust(self, roleresponse): + trustlist = [] + for Statement in roleresponse['Statement']: + for principal in Statement['Principal'].values(): + if isinstance(principal, list): + for subvalue in principal: + trustlist.append({'effect' : Statement['Effect'], 'service' : 'sts', 'action' : 'AssumeRole', 'principal' : subvalue}) + else: + trustlist.append({'effect' : Statement['Effect'], 'service' : 'sts', 'action' : 'AssumeRole', 'principal' : principal}) + + self.logger.info(trustlist) + return {'name': 'trust', 'type' : 'trust', 'statements' : trustlist} + + def process_role_attached_policies(self, attached_policies): + parsed_attached_policies = [] + for attached_policy in attached_policies: + policyresponse = self.get_policy(attached_policy['PolicyArn'])['Policy'] + self.logger.debug(str(policyresponse)) + policyversion = self.get_policy_version(attached_policy['PolicyArn'], policyresponse['DefaultVersionId'])['PolicyVersion'] + policy_document = policyversion['Document'] + + self.logger.info("Attached Policy Name: " + attached_policy['PolicyName']) + + self.logger.debug(str(policy_document)) + parsed_policy = self.parse_policy(policy_document) + parsed_attached_policies.append({'name': attached_policy['PolicyName'], 'type' : 'managed', 'statements' : parsed_policy}) + return parsed_attached_policies + + def process_role_inline_policies(self, rolename, inline_policies): + parsed_attached_policies = [] + for inline_policy_name in inline_policies: + policyresponse = self.get_role_policy(rolename, inline_policy_name) + policy_document = policyresponse['PolicyDocument'] + + self.logger.info("Inline Policy Name: " + inline_policy_name) + + self.logger.debug(str(policy_document)) + parsed_policy = self.parse_policy(policy_document) + parsed_attached_policies.append({'name': inline_policy_name, 'type' : 'inline', 'statements' : parsed_policy}) + return parsed_attached_policies + + def get_role_trust_policies(self, role_name): + parsed_trust_policies=[] + roleresponse= self.get_role(role_name ) + self.logger.info("Going to print the trust policy next") + self.logger.debug(str(roleresponse['Role']['AssumeRolePolicyDocument'])) + trustresponse = self.get_role_trust(roleresponse['Role']['AssumeRolePolicyDocument']) + self.logger.info("Going to print the trust policy next again") + self.logger.info(trustresponse) + parsed_trust_policies.append(trustresponse) + + return parsed_trust_policies + + def process_role(self, role): + parsed_policies = [] + self.logger.info("\nRole Name: " + role['RoleName']) + + #tbd call trust policies processor + trust_policy = self.get_role_trust_policies(role['RoleName']) + self.logger.debug("Going to print Processed Trust Policy") + self.logger.debug(str(trust_policy)) + parsed_policies.extend(trust_policy) + + inline_policies = self.get_role_inline_policies(role['RoleName'])['PolicyNames'] + self.logger.debug("Going to print Raw Inline Policies") + self.logger.debug(str(inline_policies)) + processed_inline_policies=self.process_role_inline_policies(role['RoleName'], inline_policies) + self.logger.debug("Going to print Processed Inline Policies") + self.logger.debug(str(processed_inline_policies)) + parsed_policies.extend(processed_inline_policies) + + attached_policies = self.get_role_attached_policies(role['RoleName'])['AttachedPolicies'] + self.logger.debug("Going to print Raw Attached Policies") + self.logger.debug(str(attached_policies)) + processed_attached_policies = self.process_role_attached_policies(attached_policies) + self.logger.debug("Going to print Processed Attached Policies") + self.logger.debug(str(processed_attached_policies)) + parsed_policies.extend(processed_attached_policies) + return parsed_policies + + def harvest_iam_roles_from_account(self): + roles=self.get_iam_roles() + self.logger.info("Number of roles: %d", len(roles)) + #bar = ProgressBar('Something') + bar = ChargingBar('Harvesting IAM Roles from '+self.account_tag, max=len(roles),suffix='%(index)d/%(max)d - %(eta)ds') + for role in roles: + parsed_policies = self.process_role(role) + self.write_out_exhaust({'name': role['RoleName'],'path':role['Path'],'policies':parsed_policies}) + bar.next() + self.close_file_handler() + bar.finish() + + def __init__(self, cli_profile_name, account_tag, output_directory): + # create self.logger, TBD change this to get logging conf based on class name + self.logger = logging.getLogger(__name__) + self.iam_reference = self.read_iam_file() + self.cli_profile_name = cli_profile_name + self.account_tag = account_tag + self.output_directory = output_directory + # Any clients created from this session will use credentials + # from the [dev] section of ~/.aws/credentials. + self.client = boto3.Session(profile_name=cli_profile_name).client('iam') + + self.filename = self.output_directory + '/' + account_tag + '_' + cli_profile_name + '_iam_tuples.csv' + self.extract_file = open(self.filename, "w", newline = '') + self.csv_out = csv.writer(self.extract_file) + self.csv_out.writerow(('rolename', 'path', 'policyname', 'policytype', 'effect', 'service', 'action', 'arn', 'principal')) \ No newline at end of file diff --git a/iamctl/iamctl.py b/iamctl/iamctl.py new file mode 100755 index 0000000..0c73e9d --- /dev/null +++ b/iamctl/iamctl.py @@ -0,0 +1,160 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +import boto3 +import json +from botocore.exceptions import ClientError +import re +import logging +import logging.config +import csv +import sys +import os +import argparse +import time +from datetime import datetime +from progress.bar import ChargingBar, Bar +from pyfiglet import Figlet +from colorama import init,Fore, Back, Style +from terminaltables import SingleTable +from os.path import expanduser +from os import path +from iamctl.harvester import Harvester +from iamctl.differ import Differ +from pkg_resources import get_distribution, DistributionNotFound + + + +def fix_me_a_directory(output): + if output is None: + output_directory = expanduser("~") + '/aws-idt/output' + time.strftime("/%Y/%m/%d/%H/%M/%S") + if not os.path.exists(output_directory): + os.makedirs(output_directory) + return output_directory + else: + return output + +def check_if_init(): + return os.path.isfile('iam.json') and os.path.isfile('equivalency_list.json') + +def harvest(profile_name,account_name,output): + if not check_if_init(): + print(Fore.YELLOW + 'Please initialize using "iamctl init"') + else: + output_directory = fix_me_a_directory(output) + harvest = Harvester(profile_name, account_name, output_directory) + #This will harvest all the iam roles from account-1 and write it to an extract file under output/ directory + harvest.harvest_iam_roles_from_account() + + +def diff(profile_name_1, account_name_1, profile_name_2, account_name_2, output): + if not check_if_init(): + print(Fore.YELLOW + 'Please initialize using "iamctl init"') + else: + output_directory = fix_me_a_directory(output) + harvest1 = Harvester(profile_name_1, account_name_1, output_directory) + harvest2 = Harvester(profile_name_2, account_name_2, output_directory) + + #This will harvest all the iam roles from account-1 and write it to an extract file under output/ directory + harvest1.harvest_iam_roles_from_account() + + #This will harvest all the iam roles from account-2 and write it to an extract file under output/ directory + harvest2.harvest_iam_roles_from_account() + + #instantiating Differ object with extract file name from each of the harvest objects for both accounts. + differ = Differ(harvest1.filename, harvest2.filename, account_name_1, account_name_2, output_directory) + + #This will generate the diff files comparing both accounts for IAM roles and prints the summary report to console + differ.generate_diff_and_summary() + +def init(): + print(Fore.BLUE + 'Initializing') + print(Fore.BLUE + 'Downloading IAM file from awspolicygen S3 Bucket: iam.json') + + #create the iam.json file + s3 = boto3.client('s3') + + response = s3.get_object(Bucket="awspolicygen", Key="js/policies.js") + stream = response['Body'] + contents = stream.read().decode('utf-8') + with open("iam.json", "w") as text_file: + text_file.write(contents[23:]) + print(Fore.BLUE + 'Creating sample equivalency list file: equivalency_list.json') + print(Style.RESET_ALL) + + #create the equivalency file + data = {} + data['accountid'] = ["123456789012","234567890123"] + data['accountprefix1'] = ["apples-production","oranges-production","apples-development","oranges-development"] + with open('equivalency_list.json', 'w') as outfile: + json.dump(data, outfile) + print(Fore.GREEN + u'\N{check mark} Done with Initialization.') + print('-> Edit the Equivalency List file to ignore known variations, prefixes while running diff.') + print('-> Run harvest or diff next.') + print(Style.RESET_ALL) + +def listprofiles(): + print(boto3.Session().available_profiles) + +def main(): + __version__ = "0.0.1" + try: + __version__ = get_distribution("iamctl").version + except DistributionNotFound: + # package is not installed + print("Hey distro not found!") + pass + + log_file_path = path.join(path.dirname(path.abspath(__file__)), 'conf/logging.conf') + logging.config.fileConfig(log_file_path) + + # create self.logger, TBD change this to get logging conf based on class name + logger = logging.getLogger(__name__) + + f = Figlet(font='bulbhead') + print(Fore.BLUE + f.renderText('IAMctl')) + print(Style.RESET_ALL) + parser = argparse.ArgumentParser(description='IAMCTL is a tool built to make it easy to export, compare and analyze AWS IAM Roles, policies across accounts. Helpful for Auditing, Archiving. See below for more use-case specific commands and their requirements. Uses AWS Boto3 SDK and AWS CLI profiles') + parser.add_argument('--version', '-V', '-v', action='version', version="%(prog)s " + __version__) + subparsers = parser.add_subparsers(dest='subparser') + + init_parser = subparsers.add_parser('init',help='Downloads the IAM service specific actions, arn format and conditions to a file named iam.json in the current folder. Also creates a sample file named equivalency_list.json which could be used to ignore known string patterns in the IAM role names to be ignored to reduce false positives while running the diff command later') + + init_parser = subparsers.add_parser('listprofiles',help='Lists all CLI profiles available') + + + harvest_parser = subparsers.add_parser('harvest', help='Downloads the IAM Roles, policies expands glob patterns, matches resources to service actions and writes the output as csv to default /aws-idt directory with a time based folder structure') + harvest_parser.add_argument('profile_name', help='AWS CLI Profile Name for Account-1') + harvest_parser.add_argument('account_name', help='Account-1 Tag [Without any Spaces]') + harvest_parser.add_argument('--output',dest ='output', help='Output directory location where files will be written to') + + diff_parser = subparsers.add_parser('diff', help='Compares the two accounts supplied as input for differences in IAM roles, policies by first harvesting from both accounts and then applying the equivalency list string patterns to ignore known false positive triggers. Write several summary level and granular observations to files to the default /aws-idt directory with a time based folder structure ') + diff_parser.add_argument('profile_name_1', help='AWS CLI Profile Name for Account-1') + diff_parser.add_argument('account_name_1', help='Account-1 Tag [Without any Spaces]') + diff_parser.add_argument('profile_name_2', help='AWS CLI Profile Name for Account-2') + diff_parser.add_argument('account_name_2', help='Account-2 Tag [Without any Spaces]') + diff_parser.add_argument('--output',dest ='output', help='Output directory location where files will be written to') + + if len(sys.argv)==1: + parser.print_help(sys.stderr) + sys.exit(1) + else: + kwargs = vars(parser.parse_args()) + globals()[kwargs.pop('subparser')](**kwargs) + + + + + + + diff --git a/media/image1.png b/media/image1.png new file mode 100755 index 0000000..5699cc1 Binary files /dev/null and b/media/image1.png differ diff --git a/media/image10.png b/media/image10.png new file mode 100755 index 0000000..8f156fa Binary files /dev/null and b/media/image10.png differ diff --git a/media/image11.png b/media/image11.png new file mode 100755 index 0000000..bcf1003 Binary files /dev/null and b/media/image11.png differ diff --git a/media/image2.png b/media/image2.png new file mode 100755 index 0000000..5a7e1d5 Binary files /dev/null and b/media/image2.png differ diff --git a/media/image3.png b/media/image3.png new file mode 100755 index 0000000..1fda86c Binary files /dev/null and b/media/image3.png differ diff --git a/media/image4.png b/media/image4.png new file mode 100755 index 0000000..f5688e6 Binary files /dev/null and b/media/image4.png differ diff --git a/media/image5.png b/media/image5.png new file mode 100755 index 0000000..d6579e8 Binary files /dev/null and b/media/image5.png differ diff --git a/media/image6.png b/media/image6.png new file mode 100755 index 0000000..1351e44 Binary files /dev/null and b/media/image6.png differ diff --git a/media/image7.png b/media/image7.png new file mode 100755 index 0000000..cb3fcf7 Binary files /dev/null and b/media/image7.png differ diff --git a/media/image8.png b/media/image8.png new file mode 100755 index 0000000..3705ff7 Binary files /dev/null and b/media/image8.png differ diff --git a/media/image9.png b/media/image9.png new file mode 100755 index 0000000..2a499e1 Binary files /dev/null and b/media/image9.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..3ab88a8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +boto3==1.9.124 +botocore==1.12.124 +colorama==0.4.1 +docutils==0.14 +jmespath==0.9.4 +progress==1.5 +pyfiglet==0.8.post1 +python-dateutil==2.8.0 +s3transfer==0.2.0 +setuptools-scm==3.3.3 +six==1.12.0 +terminaltables==3.1.0 +urllib3==1.24.1 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..87b9afa --- /dev/null +++ b/setup.py @@ -0,0 +1,52 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +from setuptools import setup + +def readme(): + with open('README.rst', encoding="utf8") as f: + return f.read() + +requires = [ + 'boto3==1.9.124', + 'botocore==1.12.124', + 'colorama==0.4.1', + 'docutils==0.14', + 'jmespath==0.9.4', + 'progress==1.5', + 'pyfiglet==0.8.post1', + 'python-dateutil==2.8.0', + 's3transfer==0.2.0', + 'setuptools-scm==3.3.3', + 'six==1.12.0', + 'terminaltables==3.1.0', + 'urllib3==1.24.1' +] + +setup(name='iamctl', + use_scm_version=True, + setup_requires=['setuptools_scm'], + #version='0.0.1', + description='IAMCTL is a tool built to make it easy to export, compare and analyze AWS IAM Roles, policies across accounts. Helpful for Auditing, Archiving. See below for more use-case specific commands and their requirements. Uses AWS Boto3 SDK and AWS CLI profiles', + long_description=readme(), + keywords='aws account roles policy iam harvester differ', + url='https://github.com/aws-samples/aws-iam-ctl', + author='Sudhir Reddy Maddulapally & Soumya Vanga', + author_email='maddulap@amazon.com, souvanga@amazon.com', + license='Apache-2.0', + install_requires=requires, + packages=['iamctl'], + zip_safe=False, + scripts=['bin/iamctl'], + include_package_data=True + ) \ No newline at end of file